Java排序--排序算法(内排序)

常用内排序算法

  我们通常所说的排序算法往往指的是内部排序算法,即需要排序的数据在计算机内存中完成整个排序的过程,当数据率超大或排序较为繁琐时常借助于计算机的硬盘对大数据进行排序工作,称之为外部排序算法

  排序算法大体可分为两种:

  1. 比较排序:时间复杂度O(nlogn) ~ O(n^2),主要有:冒泡排序选择排序插入排序归并排序堆排序快速排序等。
  2. 非比较排序:时间复杂度可以达到O(n),主要有:基数排序基数排序桶排序等。

  常见比较排序算法的性能:

  

  • 有一点我们很容易忽略的是排序算法的稳定性

  排序算法稳定性的简单形式化定义为:如果Ai = Aj,排序前Ai在Aj之前,排序后Ai还在Aj之前,则称这种排序算法是稳定的。通俗地讲就是保证排序前后两个相等的数的相对顺序不变。

  对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。

  例如,对于冒泡排序,原本是稳定的排序算法,如果将记录交换的条件改成A[i] >= A[i + 1],则两个相等的记录就会交换位置,从而变成不稳定的排序算法。

  其次,说一下排序算法稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位排序后元素的顺序在高位也相同时是不会改变的。

一、冒泡排序(Bubble Sort)

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

  冒泡排序的基本思想是,对相邻的元素进行两两比较,顺序相反则进行交换,这样,每一趟会将最小或最大的元素“浮”到顶端,最终达到完全

  代码实现

  在冒泡排序的过程中,如果某一趟执行完毕,没有做任何一次交换操作,比如数组[5,4,1,2,3],执行了两次冒泡,也就是两次外循环之后,分别将5和4调整到最终位置[1,2,3,4,5]。此时,再执行第三次循环后,一次交换都没有做,这就说明剩下的序列已经是有序的,排序操作也就可以完成了,来看下代码 

 1 /**
 2  * 冒泡排序
 3  * author tag
 4  */
 5  public static void bubbleSort(int[] arr) {
 6    for (int i = 0; i < arr.length - 1; i++) {
 7        boolean flag = true;//设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已然完成。
 8             for (int j = 0; j < arr.length - 1 - i; j++) {
 9                 if (arr[j] > arr[j + 1]) {
10                     swap(arr,j,j+1);
11                     flag = false;
12                 }
13             }
14             if (flag) {
15                 break;
16             }
17      }
18  }
19             

  上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行冒泡排序的实现过程如下

  

  使用冒泡排序为一列数字进行排序的过程如上图所示: 

  根据上面这种冒泡实现,若原数组本身就是有序的(这是最好情况),仅需n-1次比较就可完成;若是倒序,比较次数为 n-1+n-2+...+1=n(n-1)/2,交换次数和比较次数等值。所以,其时间复杂度依然为O(n2)。综合来看,冒泡排序性能还还是稍差于上面那种选择排序的。

1.冒泡排序的改进:鸡尾酒排序

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

  鸡尾酒排序的代码如下:

 1 /**
 2  * 鸡尾酒排序
 3  * @param arr
 4  */
 5 public static void cocktailSort(int[] arr) {
 6     int left = 0, right = arr.length - 1;
 7     while (left < right) {
 8         boolean flag = true;
 9         for (int i = left; i < right; i++) {
10             if (arr[i] > arr[i + 1]) {
11                 int temp = arr[i];
12                 arr[i] = arr[i + 1];
13                 arr[i + 1] = temp;
14                 flag = false;
15             }
16         }
17         right--;
18         if (flag) {
19             break; // 当数组有序时减少一半无用的循环
20         } else {
21             flag = true;
22         }
23         for (int i = right; i > left; i--) {
24             if (arr[i] < arr[i - 1]) {
25                 int temp = arr[i];
26                 arr[i] = arr[i - 1];
27                 arr[i - 1] = temp;
28                 flag = false;
29             }
30         }
31         left++;
32         if (flag) {
33             break; // 当数组有序时减少一半无用的循环
34         }
35     }
36 }

   使用鸡尾酒排序为一列数字进行排序的过程如下图所示:

  

  以序列(2,3,4,5,1)为例,鸡尾酒排序只需要访问“一次”序列就可以完成排序,但如果使用冒泡排序则需要四次。但是在乱数序列的状态下,鸡尾酒排序与冒泡排序的效率都很差劲。上述两个排序算法效果差异不大,只是鸡尾酒排序每次都通过来回交换缩小范围,而普通冒泡排序每次都从头开始,所以总的循环次数是差不多的。


二、选择排序(Selection Sort)

  选择排序是最简单直观的一种算法,基本思想为每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素,直到所有元素排完为止,简单选择排序是不稳定排序。   

  在算法实现时,每一趟确定最小元素的时候会通过不断地比较交换来使得首位置为当前最小,交换是个比较耗时的操作。其实我们很容易发现,在还未完全确定当前最小元素之前,这些交换都是无意义的。我们可以通过设置一个变量min,每一次比较仅存储较小元素的数组下标,当轮循环结束之后,那这个变量存储的就是当前最小元素的下标,此时再执行交换操作即可。

  选择排序的代码如下:

 1 /**
 2  * 简单选择排序
 3  *
 4  * @param arr
 5  */
 6  public static void selectSort(int[] arr) {
 7     for (int i = 0; i < arr.length - 1; i++) {
 8         int min = i;//每一趟循环比较时,min用于存放较小元素的数组下标,这样当前批次比较完毕最终存放的就是此趟内最小的元素的下标,避免每次遇到较小元素都要进行交换。
 9         for (int j = i + 1; j < arr.length; j++) {
10             if (arr[j] < arr[min]) {
11                 min = j;
12             }
13         }
14         //进行交换,如果min发生变化,则进行交换
15         if (min != i) {
16             swap(arr,min,i);
17         }
18     }
19  } 

  上述代码对序列{ 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }进行选择排序的实现过程如下图

       

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

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

  简单选择排序通过上面优化之后,无论数组原始排列如何,比较次数是不变的;对于交换操作,在最好情况下也就是数组完全有序的时候,无需任何交换移动,在最差情况下,也就是数组倒序的时候,交换次数为n-1次。综合下来,时间复杂度为O(n2)。


三、插入排序(Insertion Sort)

  直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。


  插入排序的代码如下:

 1 /**
 2  * 插入排序
 3  *
 4  * @param arr
 5  */
 6  public static void insertionSort(int[] arr) {
 7     for (int i = 1; i < arr.length; i++) {
 8         int j = i;
 9         while (j > 0 && arr[j] < arr[j - 1]) {
10             swap(arr,j,j-1);
11             j--;
12          }
13     }
14  }

  上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行插入排序的实现过程如下

     

  使用插入排序为一列数字进行排序的宏观过程如上图:

  简单插入排序在最好情况下,需要比较n-1次,无需交换元素,时间复杂度为O(n);在最坏情况下,时间复杂度依然为O(n2)。但是在数组元素随机排列的情况下,插入排序还是要优于上面两种排序的。

1.插入排序的改进:二分插入排序

  对于插入排序,如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的次数,我们称为二分插入排序,代码如下:

 1 /**
 2  * 二分插入排序
 3  * @param arr
 4  */
 5 public static void binaryInsertSort(int[] arr) {
 6     for (int i = 1; i < arr.length; i++) {
 7         int get = arr[i];  // 右手抓到一张扑克牌
 8         int left = 0;  // 拿在左手上的牌总是排序好的,所以可以用二分法
 9         int right = i - 1;  // 手牌左右边界进行初始化
10         while (left <= right) { // 采用二分法定位新牌的位置
11             int mid = (left + right) / 2;
12             if (arr[mid] > get)
13                 right = mid - 1;
14             else
15                 left = mid + 1;
16         }
17         for (int j = i - 1; j >= left; j--){  // 将欲插入新牌位置右边的牌整体向右移动一个单位
18             arr[j + 1] = arr[j];
19         }
20         arr[left] = get; // 将抓到的牌插入手牌
21     }
22 }

  当n较大时,二分插入排序的比较次数比直接插入排序的最差情况好得多,但比直接插入排序的最好情况要差,所当以元素初始序列已经接近升序时,直接插入排序比二分插入排序比较次数少。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。


四、插入排序的更高效改进:希尔排序(Shell Sort)

  希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。

  a.基本思想

  简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。

  我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。

  b.代码实现  

  在希尔排序的理解时,我们倾向于对于每一个分组,逐组进行处理,但在代码实现中,我们可以不用这么按部就班地处理完一组再调转回来处理下一组(这样还得加个for循环去处理分组)比如[5,4,3,2,1,0] ,首次增量设gap=length/2=3,则为3组[5,2] [4,1] [3,0],实现时不用循环按组处理,我们可以从第gap个元素开始,逐个跨组处理。同时,在插入数据时,可以采用元素交换法寻找最终位置,也可以采用数组元素移动法寻觅。希尔排序的代码比较简单,如下:

 1 package sortdemo;
 2 
 3 import java.util.Arrays;
 4 
 5 /**
 6  * author tag
 7  */
 8 public class ShellSort {
 9     public static void main(String []args){
10         int []arr ={1,4,2,7,9,8,3,6};
11         sort(arr);
12         System.out.println(Arrays.toString(arr));
13         int []arr1 ={1,4,2,7,9,8,3,6};
14         sort1(arr1);
15         System.out.println(Arrays.toString(arr1));
16     }
17 
18     /**
19      * 希尔排序 针对有序序列在插入时采用交换法
20      * @param arr
21      */
22     public static void sort(int []arr){
23         //增量gap,并逐步缩小增量
24        for(int gap=arr.length/2;gap>0;gap/=2){
25            //从第gap个元素,逐个对其所在组进行直接插入排序操作
26            for(int i=gap;i<arr.length;i++){
27                int j = i;
28                while(j-gap>=0 && arr[j]<arr[j-gap]){
29                    //插入排序采用交换法
30                    swap(arr,j,j-gap);
31                    j-=gap;
32                }
33            }
34        }
35     }
36 
37     /**
38      * 希尔排序 针对有序序列在插入时采用移动法。
39      * @param arr
40      */
41     public static void sort1(int []arr){
42         //增量gap,并逐步缩小增量
43         for(int gap=arr.length/2;gap>0;gap/=2){
44             //从第gap个元素,逐个对其所在组进行直接插入排序操作
45             for(int i=gap;i<arr.length;i++){
46                 int j = i;
47                 int temp = arr[j];
48                 if(arr[j]<arr[j-gap]){
49                     while(j-gap>=0 && temp<arr[j-gap]){
50                         //移动法
51                         arr[j] = arr[j-gap];
52                         j-=gap;
53                     }
54                     arr[j] = temp;
55                 }
56             }
57         }
58     }
59     /**
60      * 交换数组元素
61      * @param arr
62      * @param a
63      * @param b
64      */
65     public static void swap(int []arr,int a,int b){
66         arr[a] = arr[a]+arr[b];
67         arr[b] = arr[a]-arr[b];
68         arr[a] = arr[a]-arr[b];
69     }
70 }

   以23, 10, 4, 1的步长序列进行希尔排序如下图:

  

  希尔排序是不稳定的排序算法,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱。

  比如序列:{ 3, 5, 10, 8, 7, 2, 8, 1, 20, 6 },h=2时分成两个子序列 { 3, 10, 7, 8, 20 } 和  { 5, 8, 2, 1, 6 } ,未排序之前第二个子序列中的8在前面,现在对两个子序列进行插入排序,得到 { 3, 7810, 20 } 和 { 12568 } ,即 { 3, 1, 7, 2, 8, 5, 10, 6, 20, 8 } ,两个8的相对次序发生了改变。

  本文介绍了希尔排序的基本思想及其代码实现,希尔排序中对于增量序列的选择十分重要,直接影响到希尔排序的性能。我们上面选择的增量序列{n/2,(n/2)/2...1}(希尔增量),其最坏时间复杂度依然为O(n2),一些经过优化的增量序列如Hibbard经过复杂证明可使得最坏时间复杂度为O(n3/2)。


 五、归并排序(Merge Sort)

  归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

  a.分而治之

  

   可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。

  b.合并相邻有序子序列

  再来看看阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现

  

 1 package com.tag;
 2 
 3 import java.util.Arrays;
 4 
 5 /**
 6  * author tag
 7  */
 8 public class MergeSort {
 9     public static void main(String []args){
10         int []arr = {9,8,7,6,5,4,3,2,1};
11         sort(arr);
12         System.out.println(Arrays.toString(arr));
13     }
14     public static void sort(int []arr){
15         int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
16         sort(arr,0,arr.length-1,temp);
17     }
18     private static void sort(int[] arr,int left,int right,int []temp){
19         if(left<right){
20             int mid = (left+right)/2;
21             sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
22             sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
23             merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
24         }
25     }
26     private static void merge(int[] arr,int left,int mid,int right,int[] temp){
27         int i = left;//左序列指针
28         int j = mid+1;//右序列指针
29         int t = 0;//临时数组指针
30         while (i<=mid && j<=right){
31             if(arr[i]<=arr[j]){
32                 temp[t++] = arr[i++];
33             }else {
34                 temp[t++] = arr[j++];
35             }
36         }
37         while(i<=mid){//将左边剩余元素填充进temp中
38             temp[t++] = arr[i++];
39         }
40         while(j<=right){//将右序列剩余元素填充进temp中
41             temp[t++] = arr[j++];
42         }
43         t = 0;
44         //将temp中的元素全部拷贝到原数组中
45         while(left <= right){
46             arr[left++] = temp[t++];
47         }
48     }
49 }
执行结果:[1, 2, 3, 4, 5, 6, 7, 8, 9]

  上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行归并排序的实例如下 

       

  使用归并排序为一列数字进行排序的宏观过程如上图:


六、堆排序(Heap Sort)

  堆排序是利用这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。

  堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:

同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子

该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] 

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] 

ok,了解了这些定义。接下来,我们来看看堆排序的基本思想及基本步骤:

堆排序基本思想及步骤

  堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了

步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。

a.假设给定无序序列结构如下

b.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。

c.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。

d.这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。

此时,我们就将一个无需序列构造成了一个大顶堆。

步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

a.将堆顶元素9和末尾元素4进行交换

 

b.重新调整结构,使其继续满足堆定义

c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.

后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序

再简单总结下堆排序的基本思路:

a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

 1 package com.tag;
 2 
 3 import java.util.Arrays;
 4 
 5 /**
 6  * author tag
 7  * 堆排序demo
 8  */
 9 public class HeapSort {
10     public static void main(String []args){
11         int []arr = {9,8,7,6,5,4,3,2,1};
12         sort(arr);
13         System.out.println(Arrays.toString(arr));
14     }
15     public static void sort(int []arr){
16         //1.构建大顶堆
17         for(int i=arr.length/2-1;i>=0;i--){
18             //从第一个非叶子结点从下至上,从右至左调整结构
19             adjustHeap(arr,i,arr.length);
20         }
21         //2.调整堆结构+交换堆顶元素与末尾元素
22         for(int j=arr.length-1;j>0;j--){
23             swap(arr,0,j);//将堆顶元素与末尾元素进行交换
24             adjustHeap(arr,0,j);//重新对堆进行调整
25         }
26 
27     }
28 
29     /**
30      * 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
31      * @param arr
32      * @param i
33      * @param length
34      */
35     public static void adjustHeap(int []arr,int i,int length){
36         int temp = arr[i];//先取出当前元素i
37         for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始
38             if(k+1<length && arr[k]<arr[k+1]){//如果左子结点小于右子结点,k指向右子结点
39                 k++;
40             }
41             if(arr[k] >temp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
42                 arr[i] = arr[k];
43                 i = k;
44             }else{
45                 break;
46             }
47         }
48         arr[i] = temp;//将temp值放到最终的位置
49     }
50 
51     /**
52      * 交换元素
53      * @param arr
54      * @param a
55      * @param b
56      */
57     public static void swap(int []arr,int a ,int b){
58         int temp=arr[a];
59         arr[a] = arr[b];
60         arr[b] = temp;
61     }
62 }
排序结果:[1, 2, 3, 4, 5, 6, 7, 8, 9] 

堆排序算法的演示:

  

  动画中在排序过程之前简单的表现了创建堆的过程以及堆的逻辑结构。

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

  比如序列:{ 9, 5, 7, 5 },堆顶元素是9,堆排序下一步将9和第二个5进行交换,得到序列 { 5, 5, 7, 9 },再进行堆调整得到{ 7, 5, 5, 9 },重复之前的操作最后得到{ 5, 5, 7, 9 }从而改变了两个5的相对次序。

  堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。


 七、快速排序(Quick Sort)

  快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

  快速排序基本步骤如下:

  a.三数取中

  在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。

  b.根据枢纽值进行分割

 1 package com.tag;
 2 
 3 import java.util.Arrays;
 4 
 5 /**
 6  * author tag
 7  * 快速排序
 8  */
 9 public class QuickSort {
10     public static void main(String[] args) {
11         int[] arr = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
12         quickSort(arr, 0, arr.length - 1);
13         System.out.println("排序结果:" + Arrays.toString(arr));
14     }
15 
16     /**
17      * @param arr
18      * @param left  左指针
19      * @param right 右指针
20      */
21     public static void quickSort(int[] arr, int left, int right) {
22         if (left < right) {
23             //获取枢纽值,并将其放在当前待处理序列末尾
24             dealPivot(arr, left, right);
25             //枢纽值被放在序列末尾
26             int pivot = right - 1;
27             //左指针
28             int i = left;
29             //右指针
30             int j = right - 1;
31             while (true) {
32                 while (arr[++i] < arr[pivot]) {
33                 }
34                 while (j > left && arr[--j] > arr[pivot]) {
35                 }
36                 if (i < j) {
37                     swap(arr, i, j);
38                 } else {
39                     break;
40                 }
41             }
42             if (i < right) {
43                 swap(arr, i, right - 1);
44             }
45             quickSort(arr, left, i - 1);
46             quickSort(arr, i + 1, right);
47         }
48 
49     }
50 
51     /**
52      * 处理枢纽值
53      *
54      * @param arr
55      * @param left
56      * @param right
57      */
58     public static void dealPivot(int[] arr, int left, int right) {
59         int mid = (left + right) / 2;
60         if (arr[left] > arr[mid]) {
61             swap(arr, left, mid);
62         }
63         if (arr[left] > arr[right]) {
64             swap(arr, left, right);
65         }
66         if (arr[right] < arr[mid]) {
67             swap(arr, right, mid);
68         }
69         swap(arr, right - 1, mid);
70     }
71 
72     /**
73      * 交换元素通用处理
74      *
75      * @param arr
76      * @param a
77      * @param b
78      */
79     private static void swap(int[] arr, int a, int b) {
80         int temp = arr[a];
81         arr[a] = arr[b];
82         arr[b] = temp;
83     }
84 }
排序结果:[1, 2, 3, 4, 5, 6, 7, 8]

  使用快速排序法对一列数字进行排序的过程:

  

  快速排序是不稳定的排序算法,不稳定发生在基准元素与A[tail+1]交换的时刻。

  比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基准元素是5,一次划分操作后5要和第一个8进行交换,从而改变了两个元素8的相对次序。

  快速排序是一种交换类的排序,它同样是分治法的经典体现。在一趟排序中将待排序的序列分割成两组,其中一部分记录的关键字均小于另一部分。然后分别对这两组继续进行排序,以使整个序列有序。在分割的过程中,枢纽值的选择至关重要,本文采取了三位取中法,可以很大程度上避免分组"一边倒"的情况。快速排序平均时间复杂度也为O(nlogn)级。

 

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

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

posted @ 2018-08-04 13:31  北海之北  阅读(356)  评论(0编辑  收藏  举报