排序算法
Posted 小月亮的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序算法相关的知识,希望对你有一定的参考价值。
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
关于时间复杂度:
- 平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
- 线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
- O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
- 线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
关于稳定性:
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
名词解释:
n:数据规模
k:“桶”的个数
In-place:占用常数内存,不占用额外内存
Out-place:占用额外内存
稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
采用了《算法4》中的写法
1 // 比较两个元素的大小 2 public static boolean less(Comparable v, Comparable w){ 3 return v.compareTo(w) < 0; 4 } 5 6 // 交换两个元素 7 public static void exch(Comparable[] arr, int i, int j){ 8 Comparable temp = arr[i]; 9 arr[i] = arr[j]; 10 arr[j] = temp; 11 }
Bubble Sort
算法思想:从第一个元素开始,不断和隔壁的元素进行比较,最后最大的元素会到达末尾,就像最大的泡泡会跑到水面。
这样会导致,数组后面的是排好序的。所以第二次循环不用比较后面的。
代码实现:
public class Bubble{ public static void Sort(Comparable[] arr){ for(int i = 0; i < arr.length-1; i++){ for(int j = 0; j < arr.length - i - 1; j++){ if(less(arr[j+1], arr[j])){ exch(arr, j, j+1); } } } } }
Selection Sort
算法思想:外层循环的每一轮都会放好未排列数据中最小的值,内层循环则是比较找出未排序数据中最小的值。结果是,前面是排序好的数据。后面是待排序的数据。
代码实现:
1 public class Selection { 2 public static void sort(Comparable[] a) { 3 4 int N = a.length; 5 for (int i = 0; i < N; i++) { 6 // Exchange a[i] with smallest entry in a[i+1...N). 7 int min = i; 8 for (int j = i+1; j < N; j++) 9 if (less(a[j], a[min])) 10 min = j; 11 exch(a, i, min); 12 } 13 } 14 }
Insertion Sort
算法思想:就像我们打牌的时候,在摸牌期间,我们每抽到一张牌,会从我们手上的牌的右手边开始看,当前的牌与排好序的牌进行比较,直到找到左边全是比当前牌小的(遍历手上已经排好序的牌),然后插入。遍历所有抽到的牌;
代码实现:
1 public class Insertion { 2 public static void sort(Comparable[] a) { 3 int N = a.length; 4 for (int i = 1; i < N; i++) { 5 // Insert a[i] among a[i-1], a[i-2], a[i-3]... .. 6 for (int j = i; j > 0 && less(a[j], a[j-1]); j--) 7 exch(a, j, j-1); 8 } 9 } 10 }
Shell sort
算法思想:要跟间隔 h 的元素相比。
代码实现:
1 public class Shell { 2 public static void sort(Comparable[] a) { 3 int N = a.length; int h = 1; 4 while (h < N/3) h = 3*h + 1; // 1, 4, 13, 40, 121, 364, 1093, ... 5 while (h >= 1) { 6 // h-sort the array. 7 for (int i = h; i < N; i++) { 8 // Insert a[i] among a[i-h], a[i-2*h], a[i-3*h]... . 9 for (int j = i; j >= h && less(a[j], a[j-h]); j -= h) 10 exch(a, j, j-h); 11 } 12 h = h/3; 13 } 14 } 15 }
Merge Sort
算法思想:
代码实现:
1 public static void merge(Comparable[] a, int lo, int mid, int hi) { 2 // Merge a[lo..mid] with a[mid+1..hi]. 3 int i = lo, j = mid+1; 4 for (int k = lo; k <= hi; k++) // Copy a[lo..hi] to aux[lo..hi]. 5 aux[k] = a[k]; 6 for (int k = lo; k <= hi; k++) // Merge back to a[lo..hi]. 7 if(i > mid) 8 a[k] = aux[j++]; 9 else if (j > hi ) 10 a[k] = aux[i++]; 11 else if (less(aux[j], aux[i])) 12 a[k] = aux[j++]; 13 else 14 a[k] = aux[i++]; 15 }
-
left half exhausted (take from the right),
-
right half exhausted (take from the left),
-
current key on right less than current key on left (take from the right),
-
Top-down mergesort
使用了分治的思想。
1 public class Merge{ 2 private static Comparable[] aux; 3 public static void sort(Comparable[] a){ 4 aux = new Comparable[a.length]; 5 sort(a, 0, a.length-1); 6 } 7 public static void sort(Comparable[] a, int lo, int hi){ 8 if(hi <= lo){ 9 return; 10 } 11 int mid = lo +(hi - lo) / 2; 12 sort(a, lo, mid); 13 sort(a, mid+1, hi); 14 merge(a, l, mid, hi); 15 } 16 }
Bottom-up mergesort
多次遍历,子数组,从 1 开始,每次翻倍。最里面的那个循环就是在遍历所有的子数组。
merge(Comparable[] a, int lo, int mid, int hi)。
hi 为什么是 Math.min(lo+sz+sz-1, N-1)? 因为 sz 是子数组的长度,那我们归并的时候的长度,当然是 sz + sz -1
但是最后为什么是 N-1呢?
1 public class MergeBU { 2 private static Comparable[] aux; // auxiliary array for merges 3 4 public static void sort(Comparable[] a) { 5 // Do lg N passes of pairwise merges. 6 int N = a.length; 7 aux = new Comparable[N]; 8 for (int sz = 1; sz < N; sz = sz+sz) // sz: subarray size 9 for (int lo = 0; lo < N-sz; lo += sz+sz) // lo: subarray index 10 merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1)); 11 } 12 }
Quick Sort
【算法 4 里面的还不太理解,以下是参考的《白话经典算法》】
算法思想:
快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用,再加上快速排序思想----分治法也确实实用,因此很多软件公司的笔试面试,包括像腾讯,微软等知名IT公司都喜欢考这个,还有大大小的程序方面的考试如软考,考研中也常常出现快速排序的身影。
总的说来,要直接默写出快速排序还是有一定难度的,因为本人就自己的理解对快速排序作了下白话解释,希望对大家理解有帮助,达到快速排序,快速搞定。
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
该方法的基本思想是:
- 1.先从数列中取出一个数作为基准数。
- 2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
- 3.再对左右区间重复第二步,直到各区间只有一个数。
虽然快速排序称为分治法,但分治法这三个字显然无法很好的概括快速排序的全部步骤。因此我的对快速排序作了进一步的说明:挖坑填数+分治法:
先来看实例吧,定义下面再给出(最好能用自己的话来总结定义,这样对实现代码会有帮助)。
以一个数组作为示例,取区间第一个数为基准数。
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
72 |
6 |
57 |
88 |
60 |
42 |
83 |
73 |
48 |
85 |
初始时,i = 0; j = 9; X = a[i] = 72
由于已经将 a[0] 中的数保存到 X 中,可以理解成在数组 a[0] 上挖了个坑,可以将其它数据填充到这来。
从j开始向前找一个比X小或等于X的数。当j=8,符合条件,将a[8]挖出再填到上一个坑a[0]中。a[0]=a[8]; i++; 这样一个坑a[0]就被搞定了,但又形成了一个新坑a[8],这怎么办了?简单,再找数字来填a[8]这个坑。这次从i开始向后找一个大于X的数,当i=3,符合条件,将a[3]挖出再填到上一个坑中a[8]=a[3]; j--;
数组变为:
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
48 |
6 |
57 |
88 |
60 |
42 |
83 |
73 |
88 |
85 |
i = 3; j = 7; X=72
再重复上面的步骤,先从后向前找,再从前向后找。
从j开始向前找,当j=5,符合条件,将a[5]挖出填到上一个坑中,a[3] = a[5]; i++;
从i开始向后找,当i=5时,由于i==j退出。
此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将X填入a[5]。
数组变为:
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
48 |
6 |
57 |
42 |
60 |
72 |
83 |
73 |
88 |
85 |
可以看出a[5]前面的数字都小于它,a[5]后面的数字都大于它。因此再对a[0…4]和a[6…9]这二个子区间重复上述步骤就可以了。
对挖坑填数进行总结:
- 1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。
- 2.j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
- 3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
- 4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
照着这个总结很容易实现挖坑填数的代码:
1 int AdjustArray(int s[], int l, int r) //返回调整后基准数的位置 2 { 3 int i = l, j = r; 4 int x = s[l]; //s[l]即s[i]就是第一个坑 5 while (i < j) 6 { 7 // 从右向左找小于x的数来填s[i] 8 while(i < j && s[j] >= x) 9 j--; 10 if(i < j) 11 { 12 s[i] = s[j]; //将s[j]填到s[i]中,s[j]就形成了一个新的坑 13 i++; 14 } 15 16 // 从左向右找大于或等于x的数来填s[j] 17 while(i < j && s[i] < x) 18 i++; 19 if(i < j) 20 { 21 s[j] = s[i]; //将s[i]填到s[j]中,s[i]就形成了一个新的坑 22 j--; 23 } 24 } 25 //退出时,i等于j。将x填到这个坑中。 26 s[i] = x; 27 28 return i; 29 }
再写分治的代码:
1 void quick_sort1(int s[], int l, int r) 2 { 3 if (l < r) 4 { 5 int i = AdjustArray(s, l, r);//先成挖坑填数法调整s[] 6 quick_sort1(s, l, i - 1); // 递归调用 7 quick_sort1(s, i + 1, r); 8 } 9 }
整理一下:
1 //快速排序 2 void quick_sort(int s[], int l, int r) 3 { 4 if (l < r) 5 { 6 //Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1 7 int i = l, j = r, x = s[l]; 8 while (i < j) 9 { 10 while(i < j && s[j] >= x) // 从右向左找第一个小于x的数 11 j--; 12 if(i < j) 13 s[i++] = s[j]; 14 15 while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数 16 i++; 17 if(i < j) 18 s[j--] = s[i]; 19 } 20 s[i] = x; 21 quick_sort(s, l, i - 1); // 递归调用 22 quick_sort(s, i + 1, r); 23 } 24 }
快速排序还有很多改进版本,如随机选择基准数,区间内数据较少时直接用另的方法排序以减小递归深度。有兴趣的筒子可以再深入的研究下。
注1,有的书上是以中间的数作为基准数的,要实现这个方便非常方便,直接将中间的数和第一个数进行交换就可以了。
堆排序
计数排序
桶排序
基数排序
参考资料
1、白话经典算法
2、《算法4》
以上是关于排序算法的主要内容,如果未能解决你的问题,请参考以下文章