经典的排序算法及时间复杂度和稳定性如下图所示(图片来自网络):
下面依次介绍各个算法的实现原理。(学习总结,如果不正确的地方,欢迎大家指出)
一、冒泡排序:
实现原理:首先取待排序数列的第一个数字,将其与第二个数字进行比较,如果第一个数字比第二个数字大,则交换这两个数字的位置;接下来取此时数列中的第二个数字和第三个数字进行比较,如果第二个数字比第三个数字大,就交换这两个数字的位置……以此类推,直到数列的倒数第二个数字和最后一个数字比较完毕。此时,数列的最后一个数字就是当前数列中的最大数字。然后,进行下一趟排序,将数列前n-1个数字中的最大数字放置在数列的倒数第二个位置上。依次操作,直到整个数列有序。
假定待排序的数列为:{5,8,7,2,6,3,4,1},排序过程如下所示:
待排序数列:5 8 7 2 6 3 4 1
8
第二趟: 5 2 6 3 4 1 7 8
第三趟: 2 5 3 4 1 6 7 8
第四趟: 2 3 4 1 5 6 7 8
第五趟: 2 3 1 4 5 6 7 8
第六趟: 2 1 3 4 5 6 7 8
第七趟: 1 2 3 4 5 6 7 8
整个排序过程是相邻之间的元素进行比较,交换位置的,两个相同元素在排序过程中其相对位置是不会发生相对变化的,因此冒泡排序是稳定排序。
实现代码:
1 public int[] bubbleSort(int[] A, int n) {
2 for(int i=0; i<n-1; i++){
3 boolean isSwaped = false;
4 for(int j=i+1; j<n; j++){
5 if(A[i] >= A[j]){
6 isSwaped = true;
7 int temp = A[i];
8 A[i] = A[j];
9 A[j] = temp;
10 }
11 }
12 if(isSwaped == false){
13 return A;
14 }
15 }
16 return A;
17 }
在实现中,添加一个标记位用于标记上一趟排序中是否有交换数字的动作发生,如果没有发生交换动作,表明当前数列已经有序,直接结束循环就可以。这样当待排序数列基本有序的情况下,可以减少排序的趟数,降低排序消耗的时间。
二、选择排序:
实现原理:首先从待排序数列中找出最小值,将其与第1个位置上的数字进行交换;然后再在2~n这个数列中找出最小值,将其与第2个位置上的数字进行交换;依次进行,直到整个数列有序。
假定待排序的数列为:{5,8,7,2,6,3,4,1},排序过程如下所示,红色代表每一趟排序中交换位置的两个元素:
待排序数列:5 8 7 2 6 3 4 1
1 8 7 2 6 3 4 5
第二趟: 1 2 7 8 6 3 4 5
1 2 3 8 6 7
1 2 3 4 6 7 8 5
1 2 3 4 5 7 8 6
1 2 3 4 5 6 8 7
1 2 3 4 5 6 7 8
稳定性其实指的就是数列中相同元素的相对位置是否会发生变化,选择排序中会将选出来的最小元素的位置与未排序序列的第一个元素进行交换,这个操作会导致相同元素的相对位置发生变化,因此其选择排序是一种不稳定的排序。
实现代码:
1 public int[] selectionSort(int[] A, int n) {
2 for(int i=0; i<n-1; i++){
3 int index = i;
4 for(int j=i+1; j<n; j++){
5 if(A[index] > A[j]){
6 index = j;
7 }
8 }
9 if(index != i){
10 int temp = A[i];
11 A[i] = A[index];
12 A[index] = temp;
13 }
14 }
15 return A;
16 }
三、插入排序:
实现原理:插入排序就是从待排序数列中取出一个元素将其插入到已经有序的数列中去,直到将待排序数列为空。在刚开始初始化的时候,有序数列为null,将待排序数列中的第一个元素直接放入有序数列中。
假定待排序的数列为:{5,8,7,2,6,3,4,1},排序过程如下所示,绿色数列为有序数列,黑色数列为待排序数列:
待排序数列:5 8 7 2 6 3 4 1
5
第一趟: 5 8
第二趟: 5 7 8 2 6 3 4 1
2 5 7 8
2 5 6 7 8
2 3 5 6 7 8
2 3 4 5 6 7 8
1 2 3 4 5 6 7 8
插入排序也不会出现相同元素相对位置变化的情况,插入排序也是一种稳定的排序。
实现代码:
1 public int[] insertionSort(int[] A, int n) {
2 for(int i=1; i<n; i++){
3 for(int j=i; j>=1; j--){
4 if(A[j] < A[j-1]){
5 int temp = A[j];
6 A[j] = A[j-1];
7 A[j-1] = temp;
8 }
9 }
10 }
11 return A;
12 }
四、希尔排序:
实现原理:希尔排序是基于插入排序改进的排序算法。希尔排序也可称为分组插入排序,通过引入步长k,将待排序序列中相隔k个位置的元素划分为一组,对每一组进行插入排序,所有组内元素排序完毕之后,第一趟排序结束;然后根据步长变换公式,将步长变为kk-1,将待排序序列中相隔kk-1个位置的元素划分为一组,对每一组进行插入排序,所有元素排序完毕,第二趟排序结束;接下来步长变为kk-2,kk-3,……直到步长变为1。当步长变为1的时候,组内元素排序完毕,排序结束。
假定待排序的数列为:{5,8,7,2,6,3,4,1},排序过程如下所示,假设起始步长k=3,步长变换公式为kk-1=kk-1,相同颜色的数字为一组:
待排序数列:5 8 7 2 6 3 4 1
步长k=3 :2 1 3 4 6 7 5 8
步长k=2 :2 1 3 4 5 7 6 8
步长k=1 :1 2 3 4 5 6 7 8
至此,希尔排序已经完毕,每组排序过程中会由元素位置移动,组内移动过程是发生在局部范围内,两个相同元素如果在不同的分组内,其相对位置就会发生变化,因此,希尔排序是一种不稳定的排序方法。在希尔排序中,步长的选择是一个很重要的因素,步长的递减变化也不唯一,只要最后一趟排序的步长为1,操作结束的数列就是有序的。
代码实现:
1 public int[] shellSort(int[] A, int n) {
2 // write code here
3 if(n==0 || n==1){
4 return A;
5 }
6 for(int gap = n/2; gap > 0; gap = gap/2){
7 for(int i=0; i<gap; i++){
8 for(int j=i+gap; j<n; j+=gap){
9 if(A[j-gap] > A[j]){
10 int k = j;
11 while(k-gap>=0 && A[k-gap] > A[k]){
12 int temp = A[k-gap];
13 A[k-gap] = A[k];
14 A[k] = temp;
15 k = k-gap;
16 }
17 }
18 }
19 }
20 }
21 return A;
22 }
此处采用的步长是取数列长度的一半,步长的变换为依次减半。
- 五、归并排序:
实现原理:归并排序也使用分组的概念,首先将待排序数列两两一组分组,对每组数列进行排序;然后将两两元素的一个分组看作一个元素,将两个元素合为一组,对每组数列进行排序;再接下来将合并后的一个分组看到一个元素,再两两结合,对每个分组排序,直到归并为一个分组,排序结束。
假定待排序的数列为:{5,8,7,2,6,3,4,1},排序过程如下所示,每一趟分组为未排序的分组,每一趟排序为每组序列排序完毕之后的状态:
待排序数列:5 8 7 2 6 3 4 1
5 8 7 2 6 3 4 1
5 8 2 7 3 6 1 4
第二趟分组:5 8 2 7 3 6 1 4
2 5 7 8 1 3 4 6
第三趟分组:2 5 7 8 1 3 4 6
12 3 4 5 6 7 8
至此,归并排序已经完毕,归并排序的趟数为log2n,由排序过程可以发现,归并排序中移动元素位置是在一个组内进行的,有序元素的相对位置是不会发生变化的,因此,归并排序也是一种稳定的排序算法。
实现代码:
1 public int[] mergeSort(int[] A, int n) {
2 // write code here
3 int i=0;
4 for (i=2; i<=n; i=2*i){
5 for(int j=0; j<n; j += i){
6 int begin = j;
7 int end = j+i-1 < n ? j+i-1 : n-1;
8 int mid = j+i/2-1 < n ? j+i/2-1 : n-1;
9 arraySort(A, begin, mid, end);
10 }
11 }
12 //当i>n时,最后一趟排序没有进行
13 arraySort(A, 0, i/2-1, n-1);
14 return A;
15 }
16
17 public void arraySort(int[] A, int begin, int mid, int end){
18 if(mid == end){
19 //此时表明只有一个组,已经有序
20 return;
21 }
22 int[] temp =new int[end-begin+1];
23 int k=0;
24 int i=begin,j=mid+1;
25 while(i<=mid && j<=end){
26 if(A[i]<=A[j]){
27 temp[k++] = A[i++];
28 }else{
29 temp[k++] = A[j++];
30 }
31 }
32 while(i<=mid){
33 temp[k++] = A[i++];
34 }
35 while(j<=end){
36 temp[k++] = A[j++];
37 }
38
39 for(i=0; i<end-begin+1; i++){
40 A[begin+i] = temp[i];
41 }
42 }
在实现过程中,可以从两个方面来考虑,一种是完全按照归并排序的思路来进行,如上所示。另一种则可以进行逆向考虑,利用递归的思想,先将待排序数列一分为二,等这两个分组排序完毕之后,再调用组排序算法进行处理;对每个分组都进行先分组后排序的操作,最终当分组内只有一个元素的时候,直接返回,具体实现代码如下(摘自牛客网网参考答案):
1 public void mergeSort(int[] arr) {
2 if (arr == null || arr.length < 2) {
3 return;
4 }
5 process(arr, 0, arr.length - 1);
6 }
7
8 public void process(int[] arr, int left, int right) {
9 if (left == right) {
10 return;
11 }
12 int mid = (left + right) / 2;
13 process(arr, left, mid);
14 process(arr, mid + 1, right);
15 merge(arr, left, mid, right);
16 }
17
18 public void merge(int[] arr, int left, int mid, int right) {
19 int[] help = new int[right - left + 1];
20 int l = left;
21 int r = mid + 1;
22 int index = 0;
23 while (l <= mid && r <= right) {
24 if (arr[l] <= arr[r]) {
25 help[index++] = arr[l++];
26 } else {
27 help[index++] = arr[r++];
28 }
29 }
30 while (l <= mid) {
31 help[index++] = arr[l++];
32 }
33 while (r <= right) {
34 help[index++] = arr[r++];
35 }
36 for (int i = 0; i < help.length; i++) {
37 arr[left + i] = help[i];
38 }
39 }
- 六、快速排序:
实现原理:快速排序的原理就是从待排序数列中找一个基准数,每一趟排序的目的就是找到基准数的最终位置,将比基准数小的元素放在基准数前面,比基准数大的元素放在基准数后面,这样待排序数列就被基准数划分为两个待排序数列;然后分别在这两个待排序数列中找基准数,确定基准数的最终位置;在被基准数划分成的小序列依次进行选基准数,确定基准数位置的操作,直到整个序列有序。
假定待排序的数列为:{5,8,7,2,6,3,4,1},排序过程如下所示,每次选择待排序数列的第一个数作为基准数:
待排序数列:5 8 7 2 6 3 4 1
1 4 2 3 5 6 7 8
第二趟排序:1 4 2 3 5 6 7 8
第三趟分组:1 2 3 4 5 6 7
1 2 3 4 5 6 7 8
至此,快速排序已经结束。在排序过程中存在非相邻元素交换位置的操作,该操作就会导致相同元素的相对位置发生变化,因此,快速排序是一种不稳定的排序算法。
实现代码:
1 public int[] quickSort(int[] A, int n) {
2 // write code here
3 if(n==0 || n==1){
4 return A;
5 }
6 subQuickSort(0, n-1, A);
7 return A;
8 }
9
10 private void subQuickSort(int begin, int end, int[] A) {
11 if(begin == end){
12 return;
13 }
14 int flag = A[begin];
15 int i=begin+1,j=end;
16 while(i<=j){
17 while(A[i]<flag && i<j){
18 i++;
19 }
20 while(A[j]>=flag && j>i){
21 j--;
22 }
23 if(i<j){
24 int temp = A[i];
25 A[i] = A[j];
26 A[j] = temp;
27 }else if(i==j){
28 if(A[i]>=flag){
29 i=i-1;
30 }
31 for(int k=begin; k<i; k++){
32 A[k] = A[k+1];
33 }
34 A[i] = flag;
35 break;
36 }
37 }
38 if(begin < i-1){
39 subQuickSort(begin, i-1, A);
40 }
41 if(end > i+1){
42 subQuickSort(i+1, end, A);
43 }
44 }
七、堆排序
实现原理:堆排序主要是通过构建大根堆或者小根堆来实现的。以大根堆举例,首先将数组初始化成大根堆,每一趟排序从堆底开始向堆顶进行堆调整,最终将待排序数列中的最大元素调整到堆顶,然后将堆顶元素与堆的最后一个元素交换位置,最后一个元素不再作为堆中的元素;接着再进行新一轮的堆调整,直到堆中剩下一个元素为止。
假定待排序的数列为:{5,8,7,2,6,3,4,1},排序过程如下所示,每次选择待排序数列的第一个数作为基准数:
初始化的堆如下:
排序过程如下:
在每一次调整过程中,包含两步,第一步将堆顶的最大元素与堆尾的最后一个元素交换位置,此时堆尾元素位于堆顶;第二步再进行堆调整。注意,与堆尾交换的堆顶元素不再参与新一轮的堆调整。在堆排序过程中,堆尾与堆顶交换、堆调整的过程中都会出现元素交叉交换位置的情况,即隔离元素进行位置交换,这就会导致相同元素的相对位置发生变化,因此,堆排序也是一种不稳定的排序算法。
实现代码:
1 public int[] heapSort(int[] A, int n) {
2 // write code here
3 if(n==0 || n==1){
4 return A;
5 }
6 for(int i=n-1; i>0; i--){
7 buildHeap(A,i);
8 //每次大根堆找出来以后,就将堆顶元素与堆尾的元素交换位置
9 swapElem(A, 0, i);
10 }
11 return A;
12 }
13
14 private void buildHeap(int[] A, int n) {
15 // TODO Auto-generated method stub
16 int i=n;
17 while(i>=1){
18 int max = A[i];
19 int left = -1, right = -1;
20 if(i%2 == 0 && i > 0){
21 //右叶子节点
22 if(A[i] > A[i/2-1]){
23 max = A[i];
24 }else{
25 max = A[i/2-1];
26 }
27 right = i;
28 i--;
29 }
30 if(i%2 != 0 && i > 0){
31 //是左叶子节点
32 if(right != -1){
33 //右结点存在
34 if(A[i] > max){
35 max = A[i];
36 }
37 }else{
38 //右结点不存在
39 if(A[i] > A[i/2]){
40 max = A[i];
41 }else{
42 max = A[i/2];
43 }
44 }
45 left = i;
46 i--;
47 }
48 if(right != -1 && max == A[right]){
49 //右叶子结点最大,就将右叶子结点与父节点进行交换
50 swapElem(A,right,right/2-1);
51 }else if(left != -1 && max == A[left]){
52 //左叶子结点最大,就将左叶子结点与父节点进行交换
53 swapElem(A,left,left/2);
54 }
55 }
56 }
57
58 private void swapElem(int[] A, int i, int j) {
59 // TODO Auto-generated method stub
60 int temp = A[i];
61 A[i] = A[j];
62 A[j] = temp;
63 }
此代码实现采用的是大根堆,小根堆与之类似。
八、计数排序
实现原理:计数排序适用于待排序数列在一定范围内的情况。假设待排序的数列位于[a,b]之间,为a和b之间的每一个数字,开辟一片内存,作为存放与该数字相等的元素;然后遍历待排序数列,将数列中的元素放到指定的内存块中;待排序数列遍历完毕之后,依次从小到大遍历各个内存块,将内存块里面存储的元素依次取出,就可得到排好序的数列。
假定待排序的数列为:{5,8,5,7,8,1,4,5,8,7,6,3,4,1},则堆排序过程如下所示:
分析待排序数列的范围为[1,8],为这个范围内的每个元素开辟一块内存用来存储待排序数列中的元素,如下图所示:
遍历待排序数列,将待排序数列中的元素放到对应的内存块中,结果如下:
最后,依次遍历各个内存块,取出其中的元素就可以得到有序数列为:{1,1,3,4,4,5,5,5,6,7,7,8,8,8}。
计数排序中是依次遍历待排序数列的,因此计数排序也是一种稳定的排序算法;排序的时间复杂度为O(n),但需要耗费的内存也较大。在实际应用场景中,可以根据实际的应用场景来选择一种合适的排序算法。
实现代码:
1 public int[] countingSort(int[] A, int n) {
2 // write code here
3 if(n == 0 || n == 1){
4 return A;
5 }
6 //计算出数组的最大值和最小值
7 int max = A[0], min = A[0];
8 for(int i=1; i<n; i++){
9 if(max < A[i]){
10 max = A[i];
11 }
12 if(min > A[i]){
13 min = A[i];
14 }
15 }
16 ArrayList<ArrayList<Integer>> countArray = new ArrayList<ArrayList<Integer>>();
17 //创建用于计数的数组
18 for(int i = min; i <= max; i++){
19 ArrayList<Integer> al = new ArrayList<Integer>();
20 countArray.add(al);
21 }
22 for(int i=0; i<n; i++){
23 countArray.get(A[i]-min).add(A[i]);
24 }
25 int j=0;
26 for(int i = min; i <= max; i++){
27 ArrayList<Integer> al = countArray.get(i-min);
28 Iterator<Integer> it = al.iterator();
29 while(it.hasNext()){
30 A[j++] = it.next().intValue();
31 }
32 }
33 return A;
34 }
九、桶排序
实现原理:假设待排序数列中的元素服从均匀分布,主要思路是给定一定数量的桶,每一个桶包含一定数值区间,当对待排序数列进行排序时,依次取每个元素,首先根据一定的映射方法计算出元素应该放置在哪个桶中,然后,对于每个不是空的桶,将桶中的元素进行排序。最后,将每个桶中的元素依次取出,得出的就是排好序的数列。
假设待排序的数列为:{78,17,39,26,72,94,21,12,23,68},则桶排序的过程如下:
将待排序数列中的元素按照映射关系k=ni/10,将元素依次放入到对应的桶中,桶内元素在插入的时候就依次与桶中元素进行比较,找到合适的位置进行插入,得出结果如下图所示:
最后,将桶中的元素依次取出,得出排序结果为:{12,17,21,23,26,39,68,72,78,94}
桶排序过程中,数列中的相同元素放置在相同的桶中,相对位置不会发生变化,因此桶排序也是一种稳定的排序算法。
实现代码:
1 public static int[] bucketSort(int[] A, int n) {
2 // write code here
3 if(n == 0 || n == 1){
4 return A;
5 }
6 //创建n个桶
7 ArrayList<ArrayList<Integer>> al = new ArrayList<ArrayList<Integer>>();
8 for(int i=0; i<=9; i++){
9 ArrayList<Integer> radixArray = new ArrayList<>();
10 al.add(radixArray);
11 }
12 //按照映射公式将数列中的元素依次放入桶中
13 for(int i=0; i<n; i++){
14 //计算该放到哪个桶中
15 int index = A[i]/10;
16 ArrayList<Integer> radixArray = al.get(index);
17 int size = radixArray.size();
18 int j=0;
19 for(; j<size; j++){
20 if(radixArray.get(j) > A[i]){
21 break;
22 }
23 }
24 radixArray.add(j, A[i]);
25 }
26 int index = 0;
27 for(int i=0; i<al.size(); i++){
28 ArrayList<Integer> radixArray = al.get(i);
29 for(int j=0; j<radixArray.size(); j++){
30 A[index++] = radixArray.get(j);
31 }
32 }
33 return A;
34 }
十、基数排序
实现原理:基数排序也成为“桶子法”,从个位开始,将待排序数列中的元素按照个位上数字的值将该元素放置到对应的“桶”中,等到待排序数列中的元素都放进0~9对应的“桶”中后,再从小到大遍历各个“桶”,将“桶”中的元素依次取出,生成新的序列;然后再从十位开始,将新生成序列中的元素按照十位上数字的值进行上述操作,然后百位、千位……直到整个数列有序。
假定待排序的数列为:{10,19,12,17,171,131,11,18,15,17,219,16,114,121},则基数排序过程如下所示:
首先找到数列中的最大值,得出最大值的位数,确定需要进行几趟操作,该数列中最大值为219,位数为3,需要进行3趟操作。
第一趟:将待排序数列中的元素依照个位数上的值,将其放置在对应的“桶”中。
然后将“桶”中的元素依次取出,得到的新序列如下:
{10,171,131,11,121,12,114,15,17,17,18,19,219}
第二趟:将新序列中的元素按照十位数上的值,将其放置在对应的“桶”中。
取出“桶”中元素,得出新序列如下:
{10,11,12,114,15,17,17,18,19,219,121,131,171}
第三趟:将新序列中的元素按照百位数上的值,将其放置在对应的“桶”中。
取出“桶”中元素,得出新序列如下:
{10,11,12,15,17,17,18,19,114,121,131,171,219}
得出的序列就是最终排好序的数列。
计数排序过程中,是将待排序数列中的每个元素都按照其某一位上的数字将其放置在对应的“桶”中取排序的,如果两个元素大小形同,那么每趟排序过程中放置的位置是相同的,因此两个相同元素的相同位置必然不会发生变化,因此,计数排序是一种稳定的排序。
实现代码:
1 public int[] radixSort(int[] A, int n) {
2 // write code here
3 if(n == 0 || n == 1){
4 return A;
5 }
6 int max = A[0];
7 for(int i = 0; i<n; i++){
8 if(A[i] > max){
9 max = A[i];
10 }
11 }
12
13 ArrayList<ArrayList<Integer>> al = new ArrayList<ArrayList<Integer>>();
14 for(int i=0; i<=9; i++){
15 ArrayList<Integer> radixArray = new ArrayList<>();
16 al.add(radixArray);
17 }
18 //获取最大数的位数
19 int count = getDigit(max);
20 for(int i = 0; i <= count; i++){
21
22 int radix = (int) Math.pow(10, i);
23 int radix2 = (int) Math.pow(10, i+1);
24 for(int j = 0; j < n; j++){
25 int index = (A[j] % radix2) / radix;
26 al.get(index).add(A[j]);
27 }
28 //填充到内存中,再依次取出
29 int index = 0;
30 for(int j=0; j<=9; j++){
31 ArrayList<Integer> radixArray = al.get(j);
32 int size = radixArray.size();
33 for(int k=0; k<size; k++){
34 A[index++] = radixArray.remove(0);
35 }
36 }
37 }
38 return A;
39 }
40
41 private int getDigit(int max) {
42 // TODO Auto-generated method stub
43 if(max/1000 != 0){
44 return 3;
45 }else if(max / 100 != 0){
46 return 2;
47 }else if(max / 10 != 0){
48 return 1;
49 }else{
50 return 0;
51 }
52 }