写在前面
最近写代码的时候用到堆排序解决一些问题,然后写的过程中发现有点手生,就想着记录一些,然后又琢磨着干脆直接整理一份排序的文章,附带对排序的分析以及实现代码,给自己练练手。
所有代码都搞懂熟练,你还会怕排序么?如果觉得有所帮助,记得点个关注和点个赞哦
冒泡排序
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素已经排序完成。
实现原理
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
算法稳定性
冒泡排序总的平均时间复杂度为 O ( N 2 ) O(N^2) O(N2) 。冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
代码实现
public void bubbleSort(int[] sourceArray){
for (int i = 1; i < sourceArray.length; i++){
// 设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。
boolean flag = true;
for (int j = 0; j < sourceArray.length - i; j++){
if (sourceArray[j] > sourceArray[j + 1]){
sourceArray[j] = sourceArray[j] ^ sourceArray[j + 1];
sourceArray[j + 1] = sourceArray[j] ^ sourceArray[j + 1];
sourceArray[j] = sourceArray[j] ^ sourceArray[j + 1];
flag = false;
}
}
if (flag){
break;
}
}
}
快速排序
快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔提出。快速排序通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。
实现原理
- 从数列中挑出一个元素,称为 “基准”(pivot),
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
算法稳定性
在平均状况下,排序个项目要 O ( n l o g n ) O(nlogn) O(nlogn) ,事实上,快速排序 O ( n l o g n ) O(nlogn) O(nlogn) 通常明显比其他算法更快。交换 a[r]
和 a[mid]
,完成一趟快速排序。在中枢元素和 a[r]
交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为5 3 3 4 3 8 9 10 11
,现在中枢元素 5
和 3(第5个元素,下标从1开始计)
交换就会把序列中,三个元素 3
的稳定性打乱(就是最后一个三到了最前面),所以快速排序是一个不稳定的排序算法。
代码实现
public void quickSort(int[] sourceArray, int left, int right){
if (left >= right) return;
int l = left;
int r = right;
int mid = sourceArray[left];
while (l < r) {
while (l < r && sourceArray[r] >= mid)
r--;
if (l < r)
sourceArray[l++] = sourceArray[r];
while (l < r && sourceArray[l] < mid)
l++;
if (l < r)
sourceArray[r--] = sourceArray[l];
}
sourceArray[l] = mid;
quickSort(sourceArray, left, l - 1);
quickSort(sourceArray, l + 1, right);
}
快速选择排序
这里单独提一下一个混合的算法,是基于快速排序而来的,类似对快速排序进行剪枝,用于计算第 K K K大(小)的数。快速选择算法的平均时间复杂度为 O ( N ) O(N) O(N)。就像快速排序那样,本算法也是 Tony Hoare 发明的,因此也被称为 Hoare选择算法。本方法大致上与快速排序相同。简便起见,注意到第 k
个最大元素也就是第 N - k
个最小元素,因此可以用第 k
小算法来解决本问题。
首先,我们选择一个枢轴,并在线性时间内定义其在排序数组中的位置。这可以通过 划分算法 的帮助来完成。
为了实现划分,沿着数组移动,将每个元素与枢轴进行比较,并将小于枢轴的所有元素移动到枢轴的左侧。
这样,在输出的数组中,枢轴达到其合适位置。所有小于枢轴的元素都在其左侧,所有大于或等于的元素都在其右侧。这样,数组就被分成了两部分。如果是快速排序算法,会在这里递归地对两部分进行快速排序,时间复杂度为 O ( N l o g N ) O(Nlog N) O(NlogN)。而在这里,由于知道要找的第 N - k
小的元素在哪部分中,我们不需要对两部分都做处理,这样就将平均时间复杂度下降到 O ( N ) O(N) O(N),最终的算法十分直接了当 :
- 随机选择一个枢轴。
- 使用划分算法将枢轴放在数组中的合适位置 pos。将小于枢轴的元素移到左边,大于等于枢轴的元素移到右边。
- 比较 pos 和 N - k 以决定在哪边继续递归处理。
选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法。
实现原理
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
算法稳定性
选择排序是 O ( n 2 ) O(n^2) O(n2) 的时间复杂度,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列 5 8 5 2 9
,我们知道第一遍选择第 1
个元素 5
会和 2
交换,那么原序列中两个 5
的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。
代码实现
public void selectSort(int[] sourceArray){
// 总共要经过 N-1 轮比较
for (int i = 0; i < sourceArray.length - 1; i++){
int cur = i;
// 每轮需要比较的次数 N-i
for (int j = i + 1; j < sourceArray.length; j++){
if (sourceArray[cur] > sourceArray[j]) {
// 记录目前能找到的最小值元素的下标
cur = j;
}
}
// 将找到的最小值和i位置所在的值进行交换
if (i != cur){
sourceArray[i] = sourceArray[i] ^ sourceArray[cur];
sourceArray[cur] = sourceArray[i] ^ sourceArray[cur];
sourceArray[i] = sourceArray[i] ^ sourceArray[cur];
}
}
}
插入排序
插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到 O ( 1 ) O(1) O(1) 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
实现原理
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
算法稳定性
平均来说插入排序算法复杂度为 O ( n 2 ) O(n^2) O(n2) ,如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的
。
代码实现
public void insertSort(int[] sourceArray){
// 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
for (int i = 1; i < sourceArray.length; i++){
// 记录要插入的数据
int temp = sourceArray[i];
// 从已经排序的序列最右边的开始比较,找到比其小的数
int j = i;
while (j > 0 && temp < sourceArray[j - 1]){
sourceArray[j] = sourceArray[j - 1];
j--;
}
// 存在比其小的数,插入
if (j != i) sourceArray[j] = temp;
}
}
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
实现原理
- 第一步:申请空间,使其大小为两个已经 排序序列之和,该空间用来存放合并后的序列
- 第二步:设定两个 指针,最初位置分别为两个已经排序序列的起始位置
- 第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针超出序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
算法稳定性
始终都是 O ( n l o g n ) O(nlogn) O(nlogn) 的时间复杂度,代价是需要额外的内存空间。可以发现,在1
个或2
个元素时,1
个元素不会交换,2
个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定性是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
代码实现
public int[] mergeSort(int[] sourceArray){
if (sourceArray.length < 2) return sourceArray;
int mid = (int) Math.floor(sourceArray.length / 2);
int[] left = Arrays.copyOfRange(sourceArray, 0, mid);
int[] right = Arrays.copyOfRange(sourceArray, mid, sourceArray.length);
return merge(mergeSort(left), mergeSort(right));
}
private int[] merge(int[] left, int[] right){
int[] nums = new int[left.length + right.length];
int l = 0;
int r = 0;
int i = 0;
while (l < left.length && r < right.length){
if (left[l] < right[r]) {
nums[i] = left[l];
l++;
}else {
nums[i] = right[r];
r++;
}
i++;
}
while (l < left.length){
nums[i] = left[l];
l++;i++;
}
while (r < right.length){
nums[i] = right[r];
r++;i++;
}
return nums;
}
基数排序
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
实现原理
假设原来有一串数值如下所示:73, 22, 93, 43, 55, 14, 28, 65, 39, 81
,首先根据个位数的数值,在走访数值时将它们分配至编号 0
到 9
的桶子中:
0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39
接下来将这些桶子中的数值重新串接起来,成为以下的数列:81, 22, 73, 93, 43, 14, 55, 65, 28, 39
,接着再进行一次分配,这次是根据十位数来分配:
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
接下来将这些桶子中的数值重新串接起来,成为以下的数列:14, 22, 28, 39, 43, 55, 65, 73, 81, 93
,这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。LSD
的基数排序适用于位数小的数列,如果位数多的话,使用MSD
的效率会比较好。MSD
的方式与LSD
相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。
算法稳定性
基数排序的时间复杂度为 O ( n l o g ( r ) m ) O (nlog(r)m) O(nlog(r)m) ,有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
代码实现
public void radixSort(int[] sourceArray){
int maxDigit = getMaxDigit(sourceArray);
int mod = 10;
int dev = 1;
for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10){
int[][] counter = new int[mod * 2][0];
for (int j = 0; j < sourceArray.length; j++){
int bucket = ((sourceArray[j] % mod) / dev) + mod;
counter[bucket] = arrayAppend(counter[bucket], sourceArray[j]);
}
int pos = 0;
for (int[] bucket : counter){
for (int value : bucket){
sourceArray[pos++] = value;
}
}
}
}
private int getMaxDigit(int[] arr){
int maxValue = getMaxValue(arr);
return getNumLength(maxValue);
}
private int getMaxValue(int[] arr){
int maxValue = arr[0];
for (int value : arr){
if (maxValue < value){
maxValue = value;
}
}
return maxValue;
}
private int getNumLength(long num){
if (num == 0){
return 1;
}
int length = 0;
for (long temp = num; temp != 0; temp /= 10){
length++;
}
return length;
}
private int[] arrayAppend(int[] arr, int value){
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
希尔排序(shell)
希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。
实现原理
先取一个小于 n
的整数 d1
作为第一个增量,把文件的全部记录分组。所有距离为 d1
的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量 d2=1(<…<d2<d1)
,即所有记录放在同一组中进行直接插入排序为止。
算法稳定性
希尔排序的时间复杂度会比 O ( n 2 ) O(n^2) O(n2) 好一些,由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
代码实现
public void shellSort(int[] sourceArray){
int gap = sourceArray.length;
while (true){
gap /= 2;
for (int i = 0; i < gap; i++){
for (int j = i + gap; j < sourceArray.length; j += gap){
int temp = sourceArray[j];
int k = j - gap;
while (k >= 0 && sourceArray[k] > temp){
sourceArray[k + gap] = sourceArray[k];
k -= gap;
}
sourceArray[k + gap] = temp;
}
}
if (gap == 1) break;
}
}
堆排序
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
实现原理
- 创建一个堆 H[0……n-1];
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2,直到堆的尺寸为 1。
算法稳定性
堆排序的平均时间复杂度为 O ( n l o g n ) Ο(nlogn) O(nlogn) 。我们知道堆的结构是节点 i
的孩子为 2 * i
和 2 * i + 1
节点,大顶堆要求父节点大于等于其 2
个子节点,小顶堆要求父节点小于等于其 2
个子节点。在一个长为 n
的序列,堆排序的过程是从第 n / 2
开始和其子节点共 3
个值选择最大(大顶堆)或者最小(小顶堆),这 3
个元素之间的选择当然不会破坏稳定性。但当为 n / 2 - 1, n / 2 - 2, ... 1
这些个父节点选择元素时,就会破坏稳定性。有可能第 n / 2
个父节点交换把后面一个元素交换过去了,而第 n / 2 - 1
个父节点把后面一个相同的元素没 有交换,那么这 2
个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
代码实现
public void heapSort(int[] sourceArray){
int len = sourceArray.length;
buildMaxHeap(sourceArray, len);
for (int i = len - 1; i > 0; i--){
swap(sourceArray, 0, i);
len--;
heapify(sourceArray, 0, len);
}
}
private void buildMaxHeap(int[] arr, int len){
for (int i = (int) Math.floor(len / 2); i >= 0; i--){
heapify(arr, i, len);
}
}
private void heapify(int[] arr, int i, int len){
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;
if (left < len && arr[left] > arr[largest]){
largest = left;
}
if (right < len && arr[right] > arr[largest]){
largest = right;
}
if (largest != i){
swap(arr, i, largest);
heapify(arr, largest, len);
}
}
private void swap(int[] arr, int i, int j){
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
计数排序
计数排序(Counting sort)是一种稳定的线性时间排序算法。计数排序使用一个额外的数组 C C C,其中第 i i i 个元素是待排序数组 A A A中值等于 i i i的元素的个数。然后根据数组 C C C来将 A A A中的元素排到正确的位置。
实现原理
- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
算法稳定性
计数排序的时间复杂度是 O ( n + k ) O(n + k) O(n+k) ,由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上 1
),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序 0
到100
之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1
的原因。所以计数排序是稳定的。
代码实现
private static void countingSort(int[] arr, int maxValue) {
int bucketLen = maxValue + 1;
int[] bucket = new int[bucketLen];
for (int value : arr) {
bucket[value]++;
}
int sortedIndex = 0;
for (int j = 0; j < bucketLen; j++) {
while (bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
}
private static int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}