参考转载:十大经典排序
0、排序算法说明(本文均小->大为例)
-
0.1排序定义
对一序列对象根据某个关键字进行排序。 -
0.2术语说明
1.稳定性 :如果a原本在b前面,而a=b,排序之后a仍然在b的前面则稳定,否则不稳定;
2.内排序 :所有排序操作都在内存中完成;
3.外排序 :排序通过磁盘和内存的数据传输才能进行(由于数据太大,因此把数据放在磁盘中);
4.时间复杂度 : 一个算法执行所耗费的时间。
5.空间复杂度 :运行完一个程序所需内存的大小。 -
0.3 算法总结
其中:n: 数据规模;k: “桶”的个数;In-place: 占用常数内存,不占用额外内存;Out-place: 占用额外内存。
简单排序算法有:1冒泡排序,2选择排序,3插入排序,4希尔排序。 -
0.5 算法分类
-
0.6 比较和非比较的区别
比较排序 :在排序的最终结果里,每个数都必须和其他数进行比较,才能确定自己的位置 。如
快速排序、归并排序、堆排序、冒泡排序
。
在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。非比较排序:通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置 。如
计数排序、基数排序、桶排序
。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。
非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。
1、冒泡排序(Bubble Sort)
遍历要排序的数列,一次比较两个元素,如果顺序错误就交换。重复直到该数列排序完成。越小的元素会经由交换慢慢“浮”到数列的顶端。
- 1.1 算法描述
步骤1: 比较相邻元素。如果第一个比第二个大,交换;
步骤2: 对每一对相邻元素作同样的工作,结果在最后的元素为最大数;
步骤3: 每次除最后一个元素外,重复以上步骤直到排序完成。 - 1.2 动图演示
- 1.3 代码实现
public static int[] bubbleSort(int[] array) {
if (array.length == 0)
return array;
for (int i = 0; i < array.length; i++)
for (int j = 0; j < array.length - 1 - i; j++)
if (array[j + 1] < array[j]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
return array;
}
- 1.4 算法分析
最佳情况:T(n) = O(n)
最差情况:T(n) = O(n2)
平均情况:T(n) = O(n2)
2、选择排序(Selection Sort)
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。
-
2.1 算法描述
步骤1:初始状态:无序区为R[1…n],有序区为空;
步骤2:第i趟排序(i=1~n-1)开始时,有序区和无序区分别为R[1…i-1]和R(i…n)。
从当前无序区中选出关键字最小的记录 R[k],与无序区的第1个记录R交换。有序区记录个数+1,无序区个数-1;
步骤3:n-1趟结束,数组有序化了。 -
2.2 动图演示
-
2.3 代码实现
public static int[] selectionSort(int[] array) {
if (array.length == 0)
return array;
for (int i = 0; i < array.length; i++) {
int minIndex = i;
for (int j = i; j < array.length; j++) {
if (array[j] < array[minIndex]) //找到最小的数
minIndex = j; //将最小数的索引保存
}
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
return array;
}
- 2.4 算法分析
最佳情况:T(n) = O(n2)
最差情况:T(n) = O(n2)
平均情况:T(n) = O(n2)
用到它的时候,数据规模越小越好。
优点:不占用额外的内存空间了,稳定。
缺点:因为无论什么数据进去都是O(n2)的时间复杂度 。
3、插入排序(Insertion Sort)
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(只需O(1)的额外空间),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
-
3.1 算法描述
步骤1: 从第一个元素开始,该元素可以认为已经被排序;
步骤2: 取出下一个元素,在已经排序的元素序列中从后向前扫描;
步骤3: 如果该元素(已排序)大于新元素,将该元素移到下一位置;
步骤4: 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
步骤5: 将新元素插入到该位置后;
步骤6: 重复步骤2~5。 -
3.2 动图演示
- 3.3 代码实现
public static int[] insertionSort(int[] array) {
if (array.length == 0)
return array;
int current;
for (int i = 0; i < array.length - 1; i++) {
current = array[i + 1];
int preIndex = i;
while (preIndex >= 0 && current < array[preIndex]) {
array[preIndex + 1] = array[preIndex];
preIndex--;
}
array[preIndex + 1] = current;
}
return array;
}
- 3.4 算法分析
最佳情况:T(n) = O(n)
最坏情况:T(n) = O(n2)
平均情况:T(n) = O(n2)
4、希尔排序(Shell Sort)
把记录按一定增量分组,对每组使用直接插入排序排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,排序完成。简单插入排序改进后的版本,能优先比较距离较远的元素,又叫缩小增量排序。
-
4.1 算法描述
选择增量gap=length/2(希尔增量,非最优),缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。
步骤1:选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
步骤2:按增量序列个数k,对序列进行k 趟排序;
步骤3:每趟排序,根据对应的增量ti,将待排序列分割成若干子序列,对各子表进行直接插入排序。增量因子为1 时,排序完成。 -
4.2 过程演示
-
4.3 代码实现
public static int[] ShellSort(int[] array) {
int len = array.length;
int temp, gap = len / 2;
while (gap > 0) {
for (int i = gap; i < len; i++) {
temp = array[i];
int preIndex = i - gap;
while (preIndex >= 0 && array[preIndex] > temp) {
array[preIndex + gap] = array[preIndex];
preIndex -= gap;
}
array[preIndex + gap] = temp;
}
gap /= 2;
}
return array;
}
- 4.4 算法分析
最佳情况:T(n) = O(nlog2 n)
最坏情况:T(n) = O(nlog2 n)
平均情况:T(n) =O(nlog2n)
5、归并排序(Merge Sort)
采用分治思想。将已有序的子序列合并,得到完全有序的序列。若将两个有序表合并成一个有序表,称为2-路归并。
-
5.1 算法描述
步骤1:把长度为n的输入序列分成两个长度为n/2的子序列;
步骤2:对这两个子序列分别采用归并排序;
步骤3:将两个排序好的子序列合并成一个最终的排序序列。 -
5.2 动图演示
public static int[] MergeSort(int[] array) {
if (array.length < 2) return array;
int mid = array.length / 2;
int[] left = Arrays.copyOfRange(array, 0, mid);
int[] right = Arrays.copyOfRange(array, mid, array.length);
return merge(MergeSort(left), MergeSort(right));
}
- 5.4 算法分析
最佳情况:T(n) = O(n)
最差情况:T(n) = O(nlogn)
平均情况:T(n) = O(nlogn)
归并排序与选择排序比,时间复杂度低,但需要额外的内存空间。
6、快速排序(Quick Sort)
通过一趟排序将待排记录分隔成独立的两部分(分治思想),其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序,无稳定性。典型应用:荷兰国旗问题。
c语言中可直接调用qsort()函数,详见比较器-各个数据结构的应用(c语言中的qsort函数),工程上少于60的数量级且类型统一均可用快排。
- 6.1 算法描述
步骤1:从数列中挑出一个元素,称为 “基准”(pivot );
步骤2:重新排序数列,所有元素比基准值小的摆放在基准前面,比基准值大摆后面(相同的数可以到任一边,相同值无差异)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
步骤3:递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。 - 6.2 动图演示
- 6.3 代码实现
public static int[] QuickSort(int[] array, int start, int end) {
if (array.length < 1 || start < 0 || end >= array.length || start > end) return null;
int smallIndex = partition(array, start, end);
if (smallIndex > start)
QuickSort(array, start, smallIndex - 1);
if (smallIndex < end)
QuickSort(array, smallIndex + 1, end);
return array;
}
public static int partition(int[] array, int start, int end) {
int pivot = (int) (start + Math.random() * (end - start + 1));
int smallIndex = start - 1;
swap(array, pivot, end);
for (int i = start; i <= end; i++)
if (array[i] <= array[end]) {
smallIndex++;
if (i > smallIndex)
swap(array, i, smallIndex);
}
return smallIndex;
}
- 6.4 算法分析
最佳情况:T(n) = O(nlogn)
最差情况:T(n) = O(n2)
平均情况:T(n) = O(nlogn)
改进快排:随机快排复杂度O(N*log2N),O(N)额外空间。
7、堆排序(Heap Sort)
利用堆这种结构所设计的一种算法。堆近似完全二叉树,子结点的键值或索引总是小于(或者大于)它的父节点。
-
7.1 算法描述
步骤1:将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
步骤2:将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
步骤3:交换后新堆顶R[1]可能违反堆性质,对当前无序区(R1,R2,……Rn-1)调整为新堆。重复直到有序区的元素个数为n-1,排序完成。 -
7.2 动图演示
-
7.3 代码实现
注意:这里用到了完全二叉树的部分性质:详情见数据结构二叉树知识点
//声明全局变量,用于记录数组array的长度;
static int len;
public static int[] HeapSort(int[] array) {
len = array.length;
if (len < 1) return array;
//1.构建一个最大堆
buildMaxHeap(array);
//2.循环将堆首位(最大值)与末位交换,然后在重新调整最大堆
while (len > 0) {
swap(array, 0, len - 1);
len--;
adjustHeap(array, 0);
}
return array;
}
//建立最大堆
public static void buildMaxHeap(int[] array) {
//从最后一个非叶子节点开始向上构造最大堆
//for循环这样写会更好一点:i的左子树和右子树分别2i+1和2(i+1)
for (int i = (len/2- 1); i >= 0; i--) {
adjustHeap(array, i);
}
}
//调整使之成为最大堆
public static void adjustHeap(int[] array, int i) {
int maxIndex = i;
//如果有左子树,且左子树大于父节点,则将最大指针指向左子树
if (i * 2 < len && array[i * 2] > array[maxIndex])
maxIndex = i * 2;
//如果有右子树,且右子树大于父节点,则将最大指针指向右子树
if (i * 2 + 1 < len && array[i * 2 + 1] > array[maxIndex])
maxIndex = i * 2 + 1;
//如果父节点不是最大值,则将父节点与最大值交换,并且递归调整与父节点交换的位置。
if (maxIndex != i) {
swap(array, maxIndex, i);
adjustHeap(array, maxIndex);
}
}
- 7.4 算法分析
最佳情况:T(n) = O(nlogn)
最差情况:T(n) = O(nlogn)
平均情况:T(n) = O(nlogn)
建立大根堆复杂度O(N)=log21 + log22 + … + log2(N-1)
8、计数排序(Counting Sort)
使用一个额外数组C,定义C[i] 为待排序数组A中值等于i的元素的个数。根据数组C重构A。要求输入的数据必须是有确定范围的整数。是一种稳定的排序算法。
-
8.1 算法描述
步骤1:找出待排序的数组中最大和最小的元素;
步骤2:C[i] = A中值为i的元素出现次数;
步骤3:对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
步骤4:反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。 -
8.2 动图演示
-
8.3 代码实现
public static int[] CountingSort(int[] array) {
if (array.length == 0) return array;
int bias, min = array[0], max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max)
max = array[i];
if (array[i] < min)
min = array[i];
}
bias = 0 - min;
int[] bucket = new int[max - min + 1];
Arrays.fill(bucket, 0);
for (int i = 0; i < array.length; i++) {
bucket[array[i] + bias]++;
}
int index = 0, i = 0;
while (index < array.length) {
if (bucket[i] != 0) {
array[index] = i - bias;
bucket[i]--;
index++;
} else
i++;
}
return array;
}
- 8.4 算法分析
最佳情况:T(n) = O(n+k)
最差情况:T(n) = O(n+k)
平均情况:T(n) = O(n+k)
当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
C的长度 =待排序数组的最大值-最小值+1,这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
9、桶排序(Bucket Sort)
假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶分别排序,再进行拼接。是计数排序的升级版,利用了函数的映射关系,高效与否在于映射函数的确定。桶可为任何类型,计数排序 和 基数排序均为其中的应用,是稳定的算法。工程上常用。
- 9.1 算法描述
步骤1:人为设置一个BucketSize,作为每个桶所能放置多少个不同数值(例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种数字,但是容量不限,即可以存放100个3);
步骤2:遍历输入数据,并且把数据一个一个放到对应的桶里去;
步骤3:对每个不是空的桶进行排序(可能使用别的排序算法 或 递归);
步骤4:从不是空的桶里把排好序的数据拼接起来。
注意,如果递归使用桶排序为各个桶排序,则当桶数量为1时要手动减小BucketSize增加下一循环桶的数量,否则会陷入死循环,导致内存溢出。
- 9.2 图片演示
- 9.3 代码实现
public static ArrayList<Integer> BucketSort(ArrayList<Integer> array, int bucketSize) {
if (array == null || array.size() < 2)
return array;
int max = array.get(0), min = array.get(0);
// 找到最大值最小值
for (int i = 0; i < array.size(); i++) {
if (array.get(i) > max)
max = array.get(i);
if (array.get(i) < min)
min = array.get(i);
}
int bucketCount = (max - min) / bucketSize + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
ArrayList<Integer> resultArr = new ArrayList<>();
for (int i = 0; i < bucketCount; i++) {
bucketArr.add(new ArrayList<Integer>());
}
for (int i = 0; i < array.size(); i++) {
bucketArr.get((array.get(i) - min) / bucketSize).add(array.get(i));
}
for (int i = 0; i < bucketCount; i++) {
if (bucketSize == 1) { // 如果带排序数组中有重复数字时
for (int j = 0; j < bucketArr.get(i).size(); j++)
resultArr.add(bucketArr.get(i).get(j));
} else {
if (bucketCount == 1)
bucketSize--;
ArrayList<Integer> temp = BucketSort(bucketArr.get(i), bucketSize);
for (int j = 0; j < temp.size(); j++)
resultArr.add(temp.get(j));
}
}
return resultArr;
}
- 9.4 算法分析
最佳情况:T(n) = O(n+k)
最差情况:T(n) = O(n+k)
平均情况:T(n) = O(n2)
桶排序最好情况下使用线性时间O(n),取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
10、基数排序(Radix Sort)
按照低位先排序,收集;然后再按照高位排序,再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。是非比较的排序算法。
- 10.1 算法描述
步骤1:取得数组中的最大数,并取得位数;
步骤2:arr为原始数组,从最低位开始取每个位组成radix数组;
步骤3:对radix进行计数排序(利用计数排序适用于小范围数的特点); - 10.2 动图演示
- 10.3 代码实现
public static int[] RadixSort(int[] array) {
if (array == null || array.length < 2)
return array;
// 1.先算出最大数的位数;
int max = array[0];
for (int i = 1; i < array.length; i++) {
max = Math.max(max, array[i]);
}
int maxDigit = 0;
while (max != 0) {
max /= 10;
maxDigit++;
}
int mod = 10, div = 1;
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++)
bucketList.add(new ArrayList<Integer>());
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
for (int j = 0; j < array.length; j++) {
int num = (array[j] % mod) / div;
bucketList.get(num).add(array[j]);
}
int index = 0;
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k < bucketList.get(j).size(); k++)
array[index++] = bucketList.get(j).get(k);
bucketList.get(j).clear();
}
}
return array;
}
-
10.4 算法分析
最佳情况:T(n) = O(nk)
最差情况:T(n) = O(nk)
平均情况:T(n) = O(nk)
对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数。 -
10.5 基数排序有两种方法:
MSD 从高位开始进行排序
LSD 从低位开始进行排序 -
基数排序 vs 计数排序 vs 桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
1.基数排序: 根据键值的每位数字来分配桶
2.计数排序: 每个桶只存储单一键值
3.桶排序: 每个桶存储一定范围的数值
各种排序方法的选择:
通常需考虑的因素有:待排序的记录个数;记录本身的大小;记录的键值分布情况等。
- 就平均时间性能而言,快速排序最佳,其所需时间最省,但快速排序在最坏情况下的时间性能不如堆排序和归并排序。
当n较大时(>60),归并排序较堆排序省
,但归并排序所需的辅助空间最大。 - 简单排序方法中,直接插入排序最简单,当待排序的结点已按键值“基本有序”且n较小时,则应采用直接插入排序或冒泡排序,直接插入排序比冒泡排序更快些,因此经常将直接插入排序和其他的排序方法结合在一起使用。
- 当n很大且键值位数较小时,采用基数排序较好;而当键值的最高位分布较均匀时,可先按其最高位将待排序结点分成若干子表,而后对各子表进行直接插入排序。
- 从方法的稳定性来比较,
直接插入排序、冒泡排序、归并排序和基数排序
是稳定的排序方法;而直接选择排序、希尔排序、堆排序和快速排序都是不稳定的排序方法。 - 若待排序的记录个数n较小时,可采用简单排序方法;若n 较大时,应采用快速排序或堆排序。
- 若待排序的记录已基本有序,可采用起泡排序。
- 文件局部有序或文件长度较小的情况下,选直接插入排序。
当待排序列基本有序时,对冒泡排序来说,若最大关键字位于序列首部,则每趟排序仅能使其“下沉”一个位置,要使其下沉到底部仍需n-1趟排序,也即时间复杂度仍为O(n2)。而对简单选择排序来说,其比较次数与待排序列的初始状态无关;归并排序要求待排序列已经部分有序,而部分有序的含义是待排序列由若干有序的子序列组成,即每个子序列必须有序,并且其时间复杂度为O(n log2n);直接插入排序在待排序列基本有序时,每趟的比较次数大为降低,也即n-1趟比较的时间复杂度由O(n2)降至O(n)。