排序算法总结

前言

几个排序算法,这里主要是为了自己时常回顾,方便复习。

  • 这里的排序算法实现的统一的函数名称为:
    void X_Sort(ElementType A[],int N)
    这里X为排序算法的名称
  • 稳定性:任意两个相等的数据,排序前后的相对位置不发生改变
  • 没有一种排序是任何情况下都表现最好的。每一种算法都有其必须存在的理由,每一种算法都是你的数据结构具有某种特征时,这种算法可能是最好的

冒泡排序

C++版本

void Bubble_Sort(ElementType A[], int N)
{    
    for(P=N-1; P>=0; P--)//这个for循环目的是针对[0,P]这个范围
    {                    //把最大值放到范围的最右端
        flag = 0;//bool is_changed = false;
        for(i=0; i<P; i++)//i为要交换的相邻两个数的左端点 
        { /* 一趟冒泡,向右(下)沉 */
           if(A[i] > A[i+1])//这里只能用>号,保证冒泡排序是稳定的 
           {
                Swap(A[i], A[i+1]);
                flag = 1; /* 标识发生了交换 */
                //is_changed = true;
            }
        }
        if(flag==0)//if(!is_changed)
            break; /* 全程无交换 */
    }
}

java版本

import java.util.Arrays;

public class BubbleSort {

    public static void bubbleSort(int[] nums,int n){
        for(int i = 0;i<=n-1;i++){
            boolean hasSwap = false;
            //j为要交换的相邻两个数的右端点
            for(int j =n-1;j>i;j--){
                // 向左(上)冒泡,浮出水面
                if(nums[j]<nums[j-1]){
                    int temp = nums[j];
                    nums[j] = nums[j-1];
                    nums[j-1] = temp;
                    hasSwap = true;
                }
            }
            if(hasSwap==false)
                break;
        }
    }

    public static void main(String[] args) {
        int[] nums = {3,6,1,9,2,2,5,44,31};
        bubbleSort(nums,nums.length);
        System.out.println(Arrays.toString(nums));

    }
}

插入排序

  • c++版本
void InsertionSort(ElementType A[], int N)
{ /* 插入排序 */
    int P, i;
    ElementType Tmp; 
    for(P=1; P<N; P++)//不断加入新的元素,在前面已经排好序的序列 
    {                 //中找到它应该被插入的位置
        Tmp = A[P];
        //下标i为试探新元素P应该放置的位置
        //从后往前试探,0位置不用试探,如果到达了0位置,直接停下来
        //不用判断,已经找到了,至于除0以外的其它位置,
       //试探时需要加上判断条件(应放置位置的前一个元素应该小于tmp,由于前面已经排好
       //序了,那么前一个元素之前的所有的元素都小于tmp)  
        for(i=P; i>0 && A[i-1]>Tmp; i--)
            A[i] = A[i-1];  
        A[i] = Tmp;  
    }
}
  • java版本
import java.util.Arrays;

public class InsertSort {
    public static void main(String[] args) {
        int[] iarr = {3, 5, 1, 1, 98, 34, 2, 23, 17, 12, 54, 49};
        insert_sort(iarr, iarr.length);
        System.out.println(Arrays.toString(iarr));

    }

    public static void insert_sort(int[] nums, int n) {
        for (int i = 1; i < n; i++) {
            int temp = nums[i];
            int j = i;
            //当候选位置j前面还有元素时
            while (j-1>= 0) {
                if (nums[j - 1] <= temp)
                    break;
                else {
                    nums[j] = nums[j - 1];
                    j--;
                }
            }
            nums[j] = temp;
        }
    }

}
  • 最好情况:顺序 \(T= O(N)\)
  • 最坏情况:逆序 \(T = O(N^2)\)
  • 稳定

冒泡排序和插入排序都是交换相邻的元素。交换2个相邻元素(仅仅影响这两个,与其它元素的逆序关系不变)正好消去1个逆序对!
插入排序:\(T(N,I) = O(N+I)\),这里\(N\)为插入排序最外层的循环,\(I\)为逆序对个数。如果序列基本有序,则插入排序简单且高效

  • 定理:任意\(N\)个不同元素组成的序列平均具有\(N(N-1)/4\)逆序对
  • 定理:任何仅以交换相邻两元素来排序的算法,其平均时间复杂度为\(\Omega (N^2)\)
  • 这意味着:要提高算法效率,我们必须
    • 每次消去不止1个逆序对!
    • 每次交换相隔较远的2个元素!

希尔排序

它的一个基本思路是

  • 利用了插入排序的简单,
  • 同时克服了插入排序每次只能交换相邻两个元素的缺点

希尔排序又叫递减增量排序算法,它是在直接插入排序算法的基础上进行改进而来的,直接插入排序在两种情况下它表现得很好,我们这里将这两种情况归纳为直接插入排序的两种性质:

  1. 当待排序的原序列中大多数元素都已有序的情况下,此时进行的元素比较和移动的次数较少
  2. 当原序列的长度很小时,即便它的所有元素都是无序的,此时进行的元素比较和移动的次数还是很少

希尔排序正是利用了直接插入排序算法的这两个性质,

  • 它首先将待排序的原序列划分成很多小的序列,称为子序列
  • 然后对这些子序列进行直接插入排序,因为每个子序列中的元素较少,所以在它们上面应用直接插入排序效率较高(利用了上面的性质2)。
  • 这样的过程可能会进行很多次,每一次都称为一趟,每一趟都将前一趟得到的整个序列划分为不同的子序列,并再次对这些子序列进行直接插入排序。
  • 最后由这些子序列组成的整个序列中的所有元素就基本有序了,此时再在整个序列上进行最后一次的直接插入排序(利用了上面的性质1),此后整个序列的排序就完成了。

举个例子:
image

  • 定义增量序列\(D_M > D_{M-1} > … > D_1 = 1\)
  • 对每个\(D_k\)进行“\(D_k\)-间隔”排序( \(k = M, M-1, … 1\))
  • 注意:\(D_k\)-间隔”有序的序列,在执行“\(D_{k-1}\)-间隔”排序后,仍然是“\(D_k\)-间隔”有序的(不太理解这一点,存疑)

希尔排序最关键的地方就是如何对整个序列进行划分,理解了这一过程就理解了整个希尔排序。子序列划分的方法必须保证对子序列进行排序后,每个元素在整个序列中的移动范围更大。这样跳跃式的位置移动,才可能让每个元素离它的最终位置较近,因而整个序列才是比较有序的
希尔排序采用的是按增量的方式进行子序列划分,将整个序列中下标值相差固定值(增量)的所有元素划分到同一个子序列中,作为一组。按照增量划分的时候,假设增量为increment,那么整个序列也将划分为increment个子序列,被分为了increment个组。可以这样理解,我们遍历整个序列中的每个元素,并为每个元素指定它所属的子序列(组)。

  • 首先是下标为0的元素,它属于第一个子序列;然后是下标为1的元素,它属于第二个子序列;以此类推,可知前increment个元素(下标为0 ~ increment-1)属于不同的子序列(组),共计increment个
  • 从下标为increment的元素开始,每一个元素的下标值减去increment都大于或等于0(周期做mod),即这些元素都属于一个已存在的子序列。
  • 因此,整个序列将被划分为increment个子序列(组)

希尔增量序列
增量序列的最后一个元素必须是1,即希尔排序的最后一趟必须是在整个序列上进行直接插入排序,这样才能保证最终的序列是有序的。最后一趟(即增量为1)开始时,整个序列都是大致有序的,因此这一趟只会进行少数元素的比较和移动

  • 原始希尔排序\(D_M=\lfloor N/2 \rfloor, D_k=\lfloor D_{k+1}/2 \rfloor\)
    最坏情况:\(T =\Theta(N^2)\)
    举个坏例子:
    image
    增量元素不互质,则小增量(也就是增量序列的前几个元素)可能根本不起作用,前面的排序白做了。
  • Hibbard增量序列
    • \(D_k=2^k–1\),相邻元素互质
    • 最坏情况:\(T =\Theta(N^{3/2})\)
    • 猜想:\(T_{avg} = O(N^{5/4})\)
  • Sedgewick增量序列
    • \(\{1, 5, 19, 41, 109, … \}\),\(9*4^i–9*2^i+1\)\(4^i–3*2^i+1\)
    • 猜想:\(T_{avg} = O(N^{7/6})\),\(T_{worst} = O(N^{4/3})\)

对子序列排序进行并发执行
在希尔排序的每一趟中,我们都需要对属于该趟的所有子序列进行排序,直到所有的子序列都是有序的。按照我们上面的思路,我们是对所有子序列按串行的方式进行排序的

  • 即先将第一个子序列排好序,
  • 然后将第二个子序列排好序,
  • 再将第三个子序列排好序。
  • 以此类推,直到该趟中所有子序列都分别是有序的。

这样在子序列之间按严格的先后顺序进行排序的方式是绝对正确的,也是十分直观的,它非常便于理解希尔排序的整体思想。按照这样的方式也很容易用代码实现希尔排序,但在很多算法书中的实现代码却是按照并发的方式对子序列进行排序

  • 先考虑多核CPU的情况,现在把每个子序列都分配给一个对应的核,并由该核对该子序列进行排序。那么这些核可以同时运行以对所有子序列进行同时排序;这是可行的,因为每个子序列之间的元素都是独立而无重叠的,每个核之间的工作不会相互影响。这种工作方式也叫做并行
  • 再考虑单核CPU的情况,它依旧能够“同时”的执行多个任务,比如早期的分时操作系统就是运行在这样的环境下。它先执行第一个任务的一部分,然后执行第二个任务的一部分,再执行第三个任务的一部分,等等。某个时刻,它又会回来执行第一个任务的另一部分、然后又执行第二个任务的另一部分,等等。这样CPU在多个任务之间快速切换,每个任务每次只占用很少的CPU时间;这样以我们人类的视角来看这些任务就好像是同时执行的,虽然实际上每个时刻只有一个任务在执行。这种工作方式也叫做并发

只要将对每个子序列进行排序都视为一个单独的任务,那么很多希尔排序的实现方式都采用了这种并发的方式。之所以这样做,

  • 可能是为了让实现代码更紧凑
  • 或者利用按行顺序访问元素的方式减少高速缓存或内存页不命中的情况

按照这样的并发方式,一趟中所有待排序的元素(它们属于不同的子序列)其实是按它们在整个序列中的顺序访问的。我们从整个序列的第i个(它的下标为i)元素开始,一次向后遍历一个元素,每遍历到一个元素就在它所在的子序列中对它进行直接插入排序,整个序列中属于同一子序列的所有元素的下标值相差increment。当整个序列中的最后一个元素被遍历到且排序后,整个序列在该趟中的所有子序列都已排好序了。此时,就可以进入希尔排序的下一趟了。

  • C++版本
void Shell_sort( ElementType A[], int N )
{ 
    for(D=N/2; D>0; D/=2)/* 希尔增量序列 */ 
    { 
       for(P=D; P<N; P++)/* 插入排序 */ 
       { 
          Tmp = A[P];//新摸进来的牌是P,将P按照插入排序的算法插入它所在的D间隔序列
          for(i=P; i>=D && A[i-D]>Tmp; i-=D)
             A[i] = A[i-D];
          A[i] = Tmp;
       }
     }
 }
/*这里代码的插入排序是将D个D间隔的子序列一起以一种统一紧凑的形式
进行插入排序,D个子序列的初始元素下标分别为:0,1,2,...D-1,后面摸进去
来的元素来一个按照它所在的D间隔子序列进行插入排序的插入过程,它的初始元素在
0,1,2,...D-1之间,可以通过不断的-D得出
*/
  • Java版本
import java.util.Arrays;

public class ShellSort {
    public static void main(String[] args) {
        int[] iarr = {7, 2, 88, 32, 67, 43, 21, 22, 0, 7, 16, 13, 26, 55};
        shell_sort(iarr, iarr.length);
        System.out.println(Arrays.toString(iarr));

    }

    public static void shell_sort(int[] nums, int n) {
        for (int increment = n / 2; increment > 0; increment /= 2) {
            for (int i = increment; i < n; i++) {
                int temp = nums[i];
                int j = i;
                //当候选位置j所在的子序列前面还有元素时
                while (j - increment >= 0) {
                    if (nums[j - increment] <= temp)
                        break;
                    else {
                        nums[j] = nums[j - increment];
                        j = j - increment;
                    }
                }
                nums[j] = temp;

            }

        }
    }
}

根据这种增量划分子序列的方式,我们可知希尔排序是不稳定的排序算法。假设原序列中有两个相同的元素,分别记为a和b,并且a在b的前面。a和b很可能被划分到不同的子序列中,对子序列分别进行排序后,在整个序列中b可能移到了a的前面。也就是说经过希尔排序后,原序列中原本相同的两个元素的相对位置在排序后发生了改变(原本是a在b之前,排序后是b在a之前),因此希尔排序是不稳定的排序算法。

选择排序

void Selection_Sort(ElementType A[], int N)
{ 
    for(i = 0; i < N; i ++) 
    {
        /* 从A[i]到A[N–1]中找最小元,并将其位置赋给MinPosition */
        MinPosition = ScanForMin( A, i, N–1 );
        Swap( A[i], A[MinPosition] );/* 将未排序部分的最小元换到有序部分的最后位置 */   
    }
}

无论如何:\(T = \Omega(N^2)\),选择排序的瓶颈在MinPosition = ScanForMin( A, i, N–1 );
分析出瓶颈,着手进行改进,那么如何快速找到最小元

堆排序

堆排序是选择算法的改进。

算法1,一个比较傻的算法

void Heap_Sort ( ElementType A[], int N )
{ 
    BuildHeap(A); /* O(N) */
    for(i=0; i<N; i++)
       TmpA[i] = DeleteMin(A); /* O(logN) */
    for(i=0; i<N; i++) /* O(N) */
        A[i] = TmpA[i];
}
  • 以空间换时间,需要额外\(O(N)\)空间,并且复制元素需要时间。
  • \(T(N ) = O(NlogN)\)

算法2
C++版本

void Swap(ElementType *a, ElementType *b )
{
    ElementType t = *a; *a = *b; *b = t;
}
  
void PercDown( ElementType A[], int p, int N )
{ 
    int Parent, Child;
    ElementType X; 
    X = A[p]; //从堆顶向下沿着最大子节点路径,将X = A[p]放到合适的位置上
    for(Parent=p; (Parent*2+1)<N; Parent=Child ) 
    {
        //左儿子
        Child = Parent * 2 + 1;
        if((Child!=N-1) && (A[Child]<A[Child+1]))
            Child++;  /* Child指向左右子结点的较大者 */
        //因为排序过程中涉及到元素的删除,需要将节点编号最后的
        //元素放在根节点下滤,而这里是>=,故根排序是不稳定的    
        if(X >= A[Child] )
            break; /* 找到了合适位置 */
        else  /* 下滤X */
            A[Parent] = A[Child];
    }
    A[Parent] = X;
}
 
void HeapSort(ElementType A[], int N) 
{ /*堆排序*/
    int i;
    //N/2-1为最后一个有儿子的父节点的下标
    //如果堆最后一个元素为一个节点的左子树(此时N为偶数),则N-1=2i+1,N-2=2i,左右两边同时除以2,N/2-1=i
    //如果堆最后一个元素为一个节点的右子树(此时N为奇数),N-1 = 2i+2,N/2=i+1.5,等式两边同时向下取整,等式仍然
    //成立,|_N/2_| = |_i+1.5_|,|_N/2_| = i+1,i = |_N/2_|-1
    for(i=N/2-1; i>=0; i--)/* 建立最大堆 BuildHeap具体的函数形式*/
        PercDown(A, i, N);     
    for(i=N-1; i>0; i--) 
    {
        /* 删除最大堆顶 */
        Swap(&A[0], &A[i]); /* 见代码7.1 */
        PercDown(A, 0, i);
    }
}

java版本

import java.util.Arrays;

public class HeapSort {
    public static void main(String[] args) {
        int[] iarr = {7, 2, 88, 32, 67, 43, 21, 22, 0, 7, 16, 13, 26, 55};
        heap_sort(iarr, iarr.length);
        System.out.println(Arrays.toString(iarr));

    }
    //要调整的部分不包括n这个下标位置,n下标位置是要调整的部分和已经调整好的部分的
    //分界线
    public static void percDown(int[] nums,int i,int n){
        int temp = nums[i];
        int parent = i;
        //当候选位置还有孩子节点时
        while(2*parent+1<n){
            int child = 2*parent+1;
            if(child+1<n && nums[child+1]>nums[child])
                child++;
            if(temp>=nums[child])
                break;
            else{
                nums[parent] = nums[child];
                parent = child;
            }
        }
        nums[parent] = temp;
    }
    public static  void heap_sort(int[] nums,int n){
        for(int i = n/2-1;i>=0;i--){
            percDown(nums,i,n);
        }
        for(int i = n-1;i>=0;i--){
            int the_max = nums[0];
            nums[0] = nums[i];
            nums[i] = the_max;
            percDown(nums,0,i);

        }
    }
}

由于我们这里是用待排序的数组原地作为堆,而用数组表示一个堆时,无论是最大堆还是最小堆,堆顶元素都是放在下标0处,删除后需要从下标0处开始调整堆,所以这里
只能使用最大堆,不能污染已经排好序的元素。
image
注意:排序时,此时\(A[0]\)存放的是元素,而不是哨兵,注意此时父子节点下标对应关系变化.这时对于第\(i\)个单元的节点,其左孩子的编号是\(2i+1\),右孩子的编号为\(2i+2\),其父节点的编号是\(\lfloor(i-1)/2\rfloor\)(这里的除法是数学中的除法,而不是编程语言中的整除).又注意到无论是建立堆还是删除堆顶元素,其核心部分都是“下滤”,所以我们把这个核心函数抽取出来,用PercDown(A,i,N)来实现对\(A[ ]\)中的前\(N\)个元素从第\(i\)个元素开始向下迁移调整的过程。

  • 定理:堆排序处理\(N\)个不同元素的随机排列的平均比较次数是\(2NlogN - O(Nlog logN)\)
  • 虽然堆排序给出最佳平均时间复杂度,但实际效果不如用Sedgewick增量序列的希尔排序
  • 堆排序是不稳定排序

归并排序

递归实现:

void merge(int a[],int temp[], int start_1, int end_1, int end_2)
{ 
    int i = start_1;
    int start_2 = end_1 + 1;
    int j = start_2;
    int index = start_1; 
    while(i <= end_1 && j <= end_2) 
    {
        if(a[i] <= A[j])
            temp[index++] = a[i++]; 
        else
            temp[index++] = a[j++]; 
    }
    while(i <= end_1)
        temp[index++] = a[i++];
    while(j <= end_2)
        temp[index++] = a[j++];  
    for(index = start_1 ; i <= end_2; index++)
        a[index] = temp[index]; 
}
 
void merge_sort_core(int a[],int temp[], int l, int r)
{   
    if(l >= r)
        return;    
    int mid = l + (r-l)/2;
    merge_sort_core(a,temp,l,mid);              
    merge_sort_core(a,temp,mid+1,r);       
    merge(a,temp,l,mid,r);    
}
 
void merge_sort(int a[], int n)
{ 
    int *temp = int new[n];
    merge_sort_core(a,temp,0,n-1);
    delete[] temp;          
}

非递归循环实现:

void merge_1(int a[],int temp[], int start_1, int end_1, int end_2)
{ 
    int i = start_1;
    int start_2 = end_1 + 1;
    int j = start_2;
    int index = start_1; 
    while(i <= end_1 && j <= end_2) 
    {
        if(a[i] <= A[j])
            temp[index++] = a[i++]; 
        else
            temp[index++] = a[j++]; 
    }
    while(i <= end_1)
        temp[index++] = a[i++];
    while(j <= end_2)
        temp[index++] = a[j++];
}
 
/* length = 当前有序子列的长度*/
/* 两两归并a[]中的相邻有序子列到temp[]中 */
void merge_pass(int a[],int temp[], int n, int length)
{ 
  //i:两两归并第一个有序子列的左端点
  //i+2*length-1:两两归并第二个有序子列的右端点
  int i = 0;
  for(; i+2*length-1 < n; i += 2*length)
  {
    merge_1(a,temp, i,i+length-1,i+2*length-1);
  }
  //剩余一个多子列但不满正好两个子列
  //i+length:两两归并第二个有序子列的左端点
  if(i+length < n) 
    merge_1(A, temp, i, i+length-1, n-1);
  /* 最后只剩不满一个子列*/
  else
  {
    for(; i < n; i++) 
      temp[i] = a[i];
  }

}
 
void merge_sort(int a[],int n)
{ 
  int *temp = int new[n];    
  length = 1;  
  while(length < n) 
  {
    merge_pass(a,temp,n,length);
    length *= 2;
    merge_pass(temp,a,n,length);
    length *= 2;
  }     
  delete[] temp; 
}
  • a[]------(merge_pass)----> temp[] ------(merge_pass)----> a[],交替进行,减少复制数据开销。
  • 为了保证最后的结果总是存放在a[]中,我们在每步的循环中做两次的merge_pass,使交替人为的周期化(2个merge_pass为一个周期),消除由于交替而产生的最后的结果的模糊性。

总结

  • 在实际应用中,归并排序不用来做内排序,归并排序在外排序时是一个很有用的工具。
  • 归并排序是稳定的排序算法
  • 归并排序的各种好处:平均复杂度是\(O(NlogN)\),最坏复杂度也是\(O(NlogN)\)
  • 千好万好,它有一点不好,它需要一个额外的空间

快速排序

快速排序在传说中,应用中最快的排序算法。在大多数情况下,对于大规模的随机数据,快速排序还是很出色的,但是前提条件是你把快速排序中的各种小细节实现的比较到位,因为快速排序的一个特点是如果你自己写的话,很容易写错,一不小心一个细节实现的不好,它就不是快速排序了,它还相当的慢。

快速排序主要应用了分而治之的思想,把一个大问题分解为两个同样小问题,最后将小问题的解按照一定的方式组合起来,得到最终大问题的解。快速排序是不稳定排序
quicksort(ElementType A[],int N):

  • pivot = 从A[]选一个主元
  • S={A[]\pivot}分成2个独立子集:\(A_1=\{ a \in S | a <= pivot \}\)\(A_2=\{ a \in S | a > pivot \}\);
  • A[] \(= quicksort(A_1,N_1)\cup \{ pivot \} \cup quicksort(A_2,N_2)\);

选主元

  • 快速排序中,主元的选取非常重要,当每次正好中分时,是快速排序的最好情况,此时\(T_{best}(N) = O( NlogN )\)
  • 一个不聪明的选择,原序列本来就是有序的,你用了一个递归实现排序,时间复杂度还是\(O(N^2)\)
    image
  • 我们可以随机的从数组中选择元素做为主元,但是rand()函数实现不便宜啊,我们比较常用的一种方式可以取头、中、尾的中位数

子集划分

  • 快速排序之所以快,是因为经过一次子集划分后,主元一次性的被放到了正确的位置上,不像插入排序,插入的位置只是临时的
  • 不对时,指针保持原地不动。当两边都发现了不对的情况后,交换两个指针所指元素。
  • break时,i的位置为最终主元应该存放的位置。
  • 如果有元素正好等于pivot
    • 我们应该停下来交换
      比如说对于全是1的序列虽然做了许多不必要的交换,但有一个好处是,最后的i和j会停在比较中间的位置,于是主元会被换到中间的位置,于是每次递归的时候,原始序列都被等分成两个等长的序列
    • 如果不理它,继续移动指针,则
      虽然避免了不必要的交换,但这样会造成每一次子集划分,主元基本上都是放在端点上,造成发生分而治之最囧的那个情况,复杂度 \(O(N^2)\)

小规模数据的处理

  • 快速排序的问题
  • 用递归(递归栈的消耗)
    • 对于小规模的数据(例如\(N\)不到100),可能还不如插入排序快
  • 解决方案
    在程序中定义一个Cutoff的阈值,当递归的数据规模充分小时,则停止递归,直接调用简单排序(例如插入排序)。
int median3( int a[], int left, int right )
{ 
    int center = (left+right) / 2;//中
    if(a[center] < a[left])
        swap(a[left],a[center]);
    if(a[right] < a[left])
        swap(a[left], a[right]);//上面两步确保最左边存放的是最小的元素
    if(a[right] < a[center])
        swap(a[center],a[right]);
    /* 此时a[left] <= a[Center] <= a[right] */
    swap(a[center], a[right-1]); /* 将基准pivot藏到右边,置身事外*/
    /* 只需要考虑a[left+1] … a[right-2] */
    return  a[right-1];  /* 返回基准pivot */
}

void quick_sort_core(int a[], int left, int right )
{ /* 核心递归函数 */ 
    int pivot, cutoff, i, j;      
    if(cutoff <= right-left) 
    { /* 如果序列元素充分多,进入快排 */
          pivot = median3( A, left, right); /* 选基准 */ 
          i = left; j = right-1;
          while(1)
          { /*将序列中比基准小的移到基准左边,大的移到右边*/
               
               while(a[++i] < pivot);
               while(a[--j] > pivot);
               if(i < j) 
                swap(a[i],a[j]);
               else 
                break;
          }
          swap(a[i],a[right-1]);    
          quick_sort_core(a,left,i-1);    /* 递归解决左边 */ 
          quick_sort_core(a, i+1,right);   /* 递归解决右边 */  
    }
    else insertion_sort(a+left,right-left+1); /* 元素太少,用简单排序 */ 
}
 
void quick_sort(int a[], int n)
{ /* 统一接口 */
     quick_sort_core(a, 0, n-1);
}

java版本:
(算法第四版实现,这个也是数据结构算法与应用中的实现方式,不过那里提前把数组最大的数swap到了length-1作为哨兵,因此去掉了i和j的检查条件)

import java.util.Arrays;

public class QuickSort {
    public static void main(String[] args) {
        int[] iarr = {7, 2, 88, 32, 67, 43, 21, 22, 0, 7, 16, 13, 26, 55};
        quick_sort_2(iarr,0, iarr.length-1);
        System.out.println(Arrays.toString(iarr));


    }
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    public static void quick_sort_1(int[] nums,int left,int right){
        if(left>=right)
            return;
        int pivot = nums[left];// 切分元素
        int i = left,j = right+1;// 左右扫描指针
        while(true){
            // 扫描左右,检查扫描是否结束并交换元素
            //如果切分元素是数组中最小或最大的那个元素,我们就要小心别让扫描指针跑出数组的边界。
            //测试条件(j == left)是冗余的,因为切分元素就是nums[left],它不可能比自己小。可以去掉的
            //由于切分元素本身就是一个哨兵(pivot不可能小于nums[left]),左侧边界的检查是多余的。
            //要去掉另一个检查,可以在打乱数组后将数组的最大元素放在nums[length-1]中。
            //该元素永远不会移动(除非和相等的元素交换),可以在所有
            //包含它的子数组中成为哨兵。 注意:在处理内部子数组时,右子数组中最左侧的元素可以作为
            //左子数组右边界的哨兵(pivot可以作为左子数组的哨兵)。
            while(nums[++i]<pivot){
                if(i==right)
                    break;
            }
            while(nums[--j]>pivot){
                if(j==left)
                    break;
            }
            if(i<j){
                swap(nums,i,j);

            }
            else
                break;
        }
        //将切分元素nums[left]放入正确的位置j
        // nums[left..j-1] <= nums[j] <= nums[j+1..right] 达成
        swap(nums,left,j);

        quick_sort_1(nums,left,j-1);    /* 递归解决左边 */
        quick_sort_1(nums, j+1,right);   /* 递归解决右边 */



    }


    public static int partition(int[] nums, int left, int right){
        int pivot = nums[left];
        int i = left;
        int j = right+1;
        while(true){
            while(nums[++i]<pivot){
                if(i==right)
                    break;
            }
            while(nums[--j]>pivot){
                if(j==left)
                    break;
            }
            if(i<j){
                swap(nums,i,j);

            }
            else
                break;
        }
        swap(nums,left,j);
        return j;

    }

    public static void quick_sort_2(int[] nums,int left,int right){
        if(left>=right)
            return;
        int j = partition(nums, left, right);
        quick_sort_2(nums,left,j-1);
        quick_sort_2(nums, j+1,right);


    }

}

posted on 2021-09-24 22:53  朴素贝叶斯  阅读(152)  评论(0)    收藏  举报

导航