目录

排序算法

排序

我们通常所说的排序算法往往指的是内部排序算法,即数据记录在内存中进行排序。 排序算法大体可分为两种: 一种是比较排序,时间复杂度O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。 另一种是非比较排序,时间复杂度可以达到O(n),主要有:计数排序,基数排序,桶排序等。

./1534819068145.png

稳定性 稳定性的简单形式化定义为:如果Ai = Aj,排序前Ai在Aj之前,排序后Ai还在Aj之前,则称这种排序算法是稳定的。通俗地讲就是保证排序前后两个相等的数的相对顺序不变。 需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。 例如,对于冒泡排序,原本是稳定的排序算法,如果将记录交换的条件改成A[i] >= A[i + 1],则两个相等的记录就会交换位置,从而变成不稳定的排序算法。

冒泡排序

它重复地走访过要排序的元素,依次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。这个算法的名字由来是因为越小(或越大)的元素会经由交换慢慢“浮”到数列的顶端。 冒泡排序算法的运作如下:

  1. 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- 如果能在内部循环第一次运行时,使用一个旗标来表示有无需要交换的可能,可以把最优时间复杂度降低到O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定
func bubbleSort(arr []int) {
	for i := 0; i < len(arr); i++ {
		for j := 0; j < len(arr)-1-i; j++ {
			if arr[j] > arr[j+1] {
				arr[j], arr[j+1] = arr[j+1], arr[j]
			}
		}
	}
}

冒泡改进(鸡尾酒排序)

鸡尾酒排序,也叫定向冒泡排序,是冒泡排序的一种改进。此算法与冒泡排序的不同处在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能。

func bubbleSort2(arr []int) {
	l, r := 0, len(arr)-1
	for ; l < r; {
		for i := l; i < r; i++ {
			if arr[i] > arr[i+1] {
				arr[i], arr[i+1] = arr[i+1], arr[i]
			}
		}
		r--
		for i := r; i > l; i-- {
			if arr[i] < arr[i-1] {
				arr[i], arr[i-1] = arr[i-1], arr[i]
			}
		}
		l++
	}
}

快速选择排序

它的工作原理很容易理解:初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- O(n^2)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定
func selectionSort(arr []int) {
	for i := 0; i < len(arr); i++ {
		minValeIndex := i
		for j := i + 1; j < len(arr); j++ {
			if arr[j] < arr[minValeIndex] {
				minValeIndex = j
			}
		}
		arr[i], arr[minValeIndex] = arr[minValeIndex], arr[i]
	}
}

选择排序是不稳定的排序算法,不稳定发生在最小元素与A[i]交换的时刻。

比如序列:{ 5, 8, 5, 2, 9 },一次选择的最小元素是2,然后把2和第一个5进行交换,从而改变了两个元素5的相对次序。

插入排序

插入排序是一种简单直观的排序算法。它的工作原理非常类似于我们抓扑克牌。 对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。 插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。 具体算法描述如下:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5
// 分类 ------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
// 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定
func insertSort(arr []int) {
	for i := 1; i < len(arr); i++ {
		toInsertValue := arr[i]
		var j int
		for j = i - 1; j >= 0; j-- {
			if arr[j] <= toInsertValue {
				break
			}
			arr[j+1] = arr[j]
		}
		arr[j+1] = toInsertValue
	}
}

插入排序升级(二分查找插入)

对于插入排序,如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的次数,我们称为二分插入排序,换句话说:原来从右往左老老实实的找,现在通过二分查找来找到要插入的位置。

// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定
func insertSort2(arr []int) {
	for i := 1; i < len(arr); i++ {
		toInsertValue := arr[i]
		l, r := 0, i-1
		for ; l <= r; {
			mid := l + (r-l)/2
			if arr[mid] < toInsertValue {
				l = mid + 1
			} else {
				r = mid - 1
			}
		}
		var j int
		for j = i - 1; j >= l; j-- {
			arr[j+1] = arr[j]
		}
		arr[j+1] = toInsertValue
	}
}

插入排序升级(希尔排序)

回想一下直接插入排序过程,排序过程中,我们可以设置一条线,左边是排好序的,右边则是一个一个等待排序, 如果最小的那个值在最右边,那么排这个最小值的时候,需要将所有元素向右边移动一位。 是否能够减少这样的移位呢? 我们不希望它是一步一步的移动,而是大步大步的移动。刚开始的时候步长比较大,逐渐缩小至1(这时就是普通的插入排序了)。最后还是插入排序,那快在哪里?意义何在?其实最后一遍已经基本有序了,几乎就是O(n),别忘了,插入排序最适合的场景就是排基本有序的数据。

  1. 分组,步长incre初始化为len/2(当然也可以设为其他)
  2. 从incre处开始依次进行插入
  3. 和普通的插入排序一样,把插入的值一一和左边已经排序好的进行比较,只不过步长不再为1,而是incre
  4. 一遍排完,缩小步长incre=incre/2,循环进行步骤1-3,知道incre=1
func insertSort3(arr []int) {
	for incre := len(arr) / 2; incre >= 1; incre = incre / 2 {
		for i := incre; i < len(arr); i++ {
			toInsertValue := arr[i]
			var j int
			for j = i - incre; j >= 0; j -= incre {
				if arr[j] <= toInsertValue {
					break
				}
				arr[j+incre] = arr[j]
			}
			arr[j+incre] = toInsertValue
		}
	}
}

归并排序

使用分而治之的思想,先把数组分成两个子数组,把两个子数组分别排序后再整合成一个有序的数组。 把两个有序的数组合成一个有序的数组:

  1. 两个指针了l,r分别指向两个有序的子数组头
  2. 分别对比指针所对应的值,小的那个存到新数组,指针向后移一位,直到把任意一个子数组比空
  3. 把剩余的那个子数组元素追加到合成的新数组后面 我们可以利用递归,先把两个子数组排序,再合并
func guibingSort(arr []int, l, r int) {
	if l >= r {
		return
	}
	mid := l + (r-l)/2
	guibingSort(arr, l, mid)
	guibingSort(arr, mid+1, r)
	merge(arr, l, r)
}
func merge(arr []int, l, r int) {
	if l >= r {
		return
	}
	len := r - l + 1
	newArr := make([]int, len)
	mid := l + (r-l)/2
	i, j := l, mid+1
	index := 0
	for ; i <= mid && j <= r; index++ {
		if arr[i] < arr[j] {
			newArr[index] = arr[i]
			i++
		} else {
			newArr[index] = arr[j]
			j++
		}
	}
	if i <= mid {//这里注意是<=
		for ; i <= mid; i++ {
			newArr[index] = arr[i]
			index++
		}
	}
	if j <= r {//这里注意是<=
		for ; j <= r; j++ {
			newArr[index] = arr[j]
			index++
		}
	}
	for index := 0; l <= r; l++ {
		arr[l] = newArr[index]
		index++
	}
}

快速排序

快速排序使用分治策略(Divide and Conquer),是冒泡排序的升级版,步骤为:

  1. 从序列中挑出一个元素,作为"基准"(pivot).
  2. 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
  3. 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。

这里实例采用挖坑法,把第一个元素拿出来作为基准保存起来,那么第一位置就是坑了,从右边开始找到比k小的,放入坑中,那么右边的位置就成了新的坑,再从左边开始找比k大的元素,放入右边的坑中,一遍下来,把k放到最后的坑中,那么就达到了左边比k都笑,右边的都比k大。

//先进行一遍排序,第一个做基准,左边的比基准小,右边的比基准大;进行一遍排序后对剩余的进行分组再进行重复
func QuickSort(arr []int, start, end int) {
	if start >= end {
		return
	}
	l, r := start, end //[l,r]为待排序的,l左边和r右边的都是已经排好的
	k := arr[l]        //基准,那么左边第一个就是坑,任何比k小的都可以放进去
	for ; r > l; {
		//从右边往左,找到比基准小的,填到坑里,那么右边的就变成了新的坑,任何比k大的都可以放进去
		for ; r > l && arr[r] >= k; r-- {

		}
		arr[l] = arr[r]
		//从左往右,找到比基准大的
		for ; r > l && arr[l] <= k; l++ {

		}
		arr[r] = arr[l]
	}
	//当for循环结束,说明l和r相遇了,那么l=r处就是最后的坑,直接把k放进去即可,这样一遍下来,k的左边比k小,右边比k大
	arr[l]=k
	//剩余的分两组,进行重复操作
	QuickSort(arr, start, l-1)
	QuickSort(arr, l+1, end)
}

快速排序是不稳定的排序算法,不稳定发生在基准元素与A[tail+1]交换的时刻。 比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基准元素是5,一次划分操作后5要和第一个8进行交换,从而改变了两个元素8的相对次序。

三路快排

三路快排是快速排序的升级版,解决了在一般快速排序中,重复的元素会放到下个分区间中再进行排序。适用于有大量重复元素的排序。 这里我们通过一次排序分为左边小于k的,中间等于k的,右边大于k的。然后再对两头的两个小区间再进行排序即可。 通过维持三个指针来控制[left, lt )小于主元(pivot),[lt, i)等于主元,[i, gt]未知,(gt, right]大于主元。 一开始,lt指向主元的位置left,gt指向right,而i从left右边接下来的第一个索引开始遍历,每当遇到一个数,就判断它与主元之间的大小关系,有三种情况:

  • 小于主元就把这个数与lt指向的数交换,然后lt,i都自增1,然后继续遍历
  • 大于主元就把这个数与gt指向的数交换,gt自减1,此时i还得不能自增,因为它不知道gt用一个什么样的元素跟它交换,所以留到下一次循环判断交换过来的这个元素的去留
  • 等于主元就不用跟谁进行交换,直接自增1就可以
func QuickSort3(arr []int, start, end int) {
	if start >= end {
		return
	}
	l, r := start, end //[start,l)为小于基准,(r,end]大于基准。都是已经排好的
	i := l + 1 //[l,i)等于基准,[i,r]等待排序
	k := arr[l]
	for ; r > l && i <= end; {
		if arr[i]<k{
			arr[l],arr[i]=arr[i],arr[l]
			l++
			i++
		}else if arr[i]>k{
			arr[r],arr[i]=arr[i],arr[r]
			r--
		}else {
			i++
		}
	}

	QuickSort3(arr, start, l-1)
	QuickSort3(arr, r+1, end)
}

堆排序

堆排序是指利用堆这种数据结构所设计的一种选择排序算法。堆是一种近似完全二叉树的结构(通常堆是通过一维数组来实现的),并满足性质:以最大堆(也叫大根堆、大顶堆)为例,其中父结点的值总是大于它的孩子节点。

计数排序

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。 比如:一个数组中公共3种值:1,2,3。我们只要统计出1个个数,2的个数,3的个数然后反向填充进数组。 算法描述

  1. 找出待排序的数组中最大和最小的元素;
  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

桶排序

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。 算法描述

  1. 设置一个定量的数组当作空桶;
  2. 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  3. 对每个不是空的桶进行排序;
  4. 从不是空的桶里把排好序的数据拼接起来

算法选择

各种排序方法比较: 简单排序中直接插入最好,快速排序最快。 当文件为正序时,直接插入和冒泡均最佳。 若要求排序稳定,则可选用归并排序。 当记录的规模较大时,为避免耗费大量的时间去移动记录,可以用链表作为存储结构。譬如插入排序、归并排序、基数排序都易于在链表上实现,使之减少记录的移动次数。但有的排序方法,如快速排序和堆排序,在链表上却难于实现

标准库排序所选用算法

java

Java系统提供的Arrays.sort函数。对于基础类型,底层使用快速排序。对于非基础类型,底层使用归并排序。请问是为什么?

答:这是考虑到排序算法的稳定性。对于基础类型,相同值是无差别的,排序前后相同值的相对位置并不重要,所以选择更为高效的快速排序,尽管它是不稳定的排序算法;而对于非基础类型,排序前后相等实例的相对位置不宜改变,所以选择稳定的归并排序。

go

go语言中对基本类型排序是sort.Sort(data Interface)方法,自定义类型用的是sort.Slice方法,他们是“快排”、“堆排序”和“希尔排序的组合使用。是不安全的,具体如下: 总体是快排把数组不断的分为子数组,但是子数组内使用的排序就可能会变了:当子数组是元素<=12时用希尔排序。当元素个数大于12时又要通过n获取一个阈值(2*ceil(lg(n+1))),如果阈值==0则使用堆排序,否则继续使用快排。

要想对自定义的类型稳定的排序可以用sort.SliceStable方法。它用的是插入排序。