十、排序算法

排序基本概念

定义

假设含有n个记录的序列为{r1,r2,…,rn},其相应的关键字分别为{k1,k2,…,kn},需确定1,2,…,n的一种排列p1,p2,…pn,使其相应的关键字满足kp1<=kp2<=…<=kpn非递减(或非递增)关系,即使得序列成为一个按关键字有序的序列{rp1,rp2,…rpn},这样的操作就称为排序。

相关概念

  • 稳定
    如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
  • 不稳定
    如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
  • 时间复杂度
    对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
  • 空间复杂度
    是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

排序算法

1、冒泡排序

冒泡排序(Bubble Sort),顾名思义,就是指越小的元素会经由相邻两两交换慢慢“浮”到数列的顶端

/// 冒泡排序
/// @param a 数组
/// @param n 数组元素个数
void bubbleSort(int a[],int n){
    int i,j,temp,flag;
    i = j = temp = 0;
    flag = 1;
    //要进行n-1次排序
    for (i = 0; i < n-1 && flag; i++) {
        flag = 0;
        for (j = n-1; j > i; j--) {
            //把数组元素两两交换,小的浮到最上面(数组的前面地址)
            if (a[j-1] > a[j]) {
                temp = a[j-1];
                a[j-1] = a[j];
                a[j] = temp;
                flag = 1;
            }
        }
    }
}
  • 复杂度:
    • 时间复杂度
      • 平均:O(n^2)
      • 最好:O(n)
      • 最坏:O(n^2)
    • 空间复杂度 O(1)
  • 稳定:
    • 稳定

2.选择排序

选择排序(Selection Sort)是一种简单直观的排序算法。它的基本思想就是,每一趟 n-i+1(i=1,2,...,n-1) 个记录中选取关键字最小的记录作为有序序列的第 i 个记录。

算法步骤:

  1. 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
  2. 在剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾;
  3. 重复步骤 2,直到所有元素排序完毕。
/// 选择排序
/// @param a 数组
/// @param n 数组元素个数
void selectSort(int a[],int n){
    int i,j,temp,min;
    for (i = 0; i < n-1; i++) {
        min = i;
        for (j = i + 1; j < n; j++) {
            if (a[j] < a[min]) {
                min = j;
            }
        }
        if (min != i) {
            temp = a[min];
            a[min] = a[i];
            a[i] = temp;
        }
    }
}
  • 复杂度:
    • 时间复杂度
      • 平均:O(n^2)
      • 最好:O(n^2)
      • 最坏:O(n^2)
    • 空间复杂度 O(1)
  • 稳定:
    • 稳定

3.插入排序

直接插入排序算法(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增加1的有序表。

/// 直接插入排序
/// @param a 数组
/// @param n 数组元素个数
void InsertSort(int a[],int n){
    int i,j,temp;
    //这里i从1开始,因为我们需要j指向它的前一个元素
    for (i = 1; i < n; i++) {
        //后面的元素大于前面的元素,不符合已排序的条件了
        if (a[i] < a[i-1]) {
            //将i位置的数据放入缓存区
            temp = a[i];
            for ( j = i-1; j >= 0 && a[j] > temp; j--) {
                /*
                 从i的前一个元素开始,发现有比缓存区数据大的,就往后挪一步
                 因为我们是从前面开始排的,所以前面一定是排好序的,
                 这里就是挪位找到temp应该插入位置的过程
                 */
                a[j+1] = a[j];
            }
            //找准了位置,将缓存区位置的数据插入
            a[j+1] = temp;
        }
    }
}
  • 复杂度:
    • 时间复杂度
      • 平均:O(n^2)
      • 最好:O(n)
      • 最坏:O(n^2)
    • 空间复杂度 O(1)
  • 稳定:
    • 稳定

4.希尔排序

希尔排序

  • 先将整个待排序列分割成若干个字序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
  • 子序列的构成不是简单地“逐段分割”,将相隔某个增量的记录组成一个子序列,让增量逐趟缩短,直到增量为 1 为止
/// 希尔排序
/// @param a 数组
/// @param n 数组元素个数
void shellSort(int a[],int n){
    int i,j,step,temp;
    //step表示分组步长,每次除以2
    step = n >> 1;
    while (step > 0) {
        for (i = step; i < n; i += step) {
            if (a[i] < a[i-step]) {
                temp = a[i];
                for (j = i - step; j >= 0 && a[j] > temp; j -= step) {
                    a[j+step] = a[j];
                }
                a[j+step] = temp;
            }
        }
        step >>= 1;
    }
}

** 复杂度:

  • 时间复杂度
    • 平均:O(n log n) ~ O(n^2)
    • 最好:O(n^1.3)
    • 最坏:O(n^2)
  • 空间复杂度 O(1)
  • 稳定:
    • 不稳定

5.堆排序

① 完全二叉树

堆是利用完全二叉树的概念来构建的。我们回顾下完全二叉树的概念:对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点位置完全相同,则这棵二叉树称为完全二叉树。如下图所示


完全二叉树索引

完全二叉树有如下特性:

  • 叶子结点只能出现在最下两层
  • 最下层的叶子一定集中在左部连续位置
  • 倒数第二层,如有叶子结点,一定都在右部连续位置
  • 如果结点度为1,则该结点只有左孩子
  • 同样结点树的二叉树,完全二叉树的深度最小
② 堆

通过如上的完全二叉树的索引我们可以发现,对于任何一个根节点的索引i,它的左孩子索引为2i,右孩子为2i+1
我们将数组a[]构建为完全二叉树的形式,让它满足:

  • a[i] >= a[2i] 并且 a[i] >= a[2i+1],这个我们称之为大顶堆
  • a[i] <= a[2i] 并且 a[i] <= a[2i+1],这个我们称之为小顶堆
  • 其中,1<=i<=n/2
  • 其中,下标i的结点与2i结点和2i+1结点是双亲和子女关系


    大顶堆

    小顶堆
③堆排序算法

堆排序算法(Heap Sort)就是利用堆进行排序的算法,它的基本思想是:

  • 将待排序的序列构造成一个大顶堆(小顶堆)
  • 此时,整个序列的最大值就是堆顶的根结点。将它移走(就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值)
  • 然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的最大值。
  • 如此反复执行,便能得到一个有序序列。
④堆排序代码实现
  • 如果我们要从小到大排序,那么我们就构建一个大顶堆,从大到小则构建一个小顶堆
  • 我们对于一个数组构建堆的过程为从下往上,从右往左,假设n为数组的个数,那么从n/2开始构建,一直到1
  • 堆排序我们的下标从1开始,所以对于数组,下标为0位置的数据是无意义的,因为它不参与排序,所以我们传入的数组个数也是实际的数组个数减1的值
void swap(int a[],int i,int j);
void buildHeap(int a[],int n);
void heapify(int a[],int i,int n);

/// 堆排序
/// @param a 数组
/// @param n 数组元素个数
void heapSort(int a[],int n){
    //将数组构建为大顶堆
    buildHeap(a, n);
    //构建成大顶堆后,下标为1的元素就是最大值
    for (int i = n; i > 1; i--) {
        //将最大值放大最后一个位置去
        swap(a, 1, i);
        /*
         去除掉最后一个后,重新把前面的构建为大顶堆
         因为之前是大顶堆,只会在下标为1的位置出现不符合
         所以我们直接从1开始
         */
        heapify(a, 1, i-1);
    }
}


/// 将一个数组构建为大顶堆
/// @param a 数组
/// @param n 数组元素个数
void buildHeap(int a[],int n){
    for (int i = n/2; i > 0; i--) {
        heapify(a, i, n);
    }
}
/// 从i结点开始构建大顶堆(从下往上构建)
/// @param a 数组
/// @param i 开始的索引
/// @param n 需要构建的个数
void heapify(int a[],int i,int n){
    int j,temp;
    //取到这个根节点的值
    temp = a[i];
    for (j = 2*i; j <= n; j *= 2) {
        /*
         索引j和j+1如果都在n的范围内,也就是判断是否左右子树都有
         j是i结点的左子树,j+1是i结点的右子树,
         这一步我们是找出左子树还是右子树的值更大
         如果右子树的值更大,那么将j定位到右子树根节点上
         */
        if (j < n && a[j+1] > a[j]) {
            j++;
        }
        //如果这个结点的值比i结点的值小,那么是符合了大顶堆的,直接退出
        if (a[j] < temp) {
            break;
        }
        //将这个点的值给放入i位置,让它符合大顶堆
        a[i] = a[j];
        //接下来就是根据j这个根节点来看它是否符合大顶堆了
        //所以将它赋值给i
        i = j;
    }
    //经过所有遍历后,将最早的根节点值放入新的位置
    a[i] = temp;
}
/// 交换数组下标为i和j的两个元素
/// @param a 数组名
/// @param i 下标
/// @param j 下标
void swap(int a[],int i,int j){
    int temp = a[i];
    a[i] = a[j];
    a[j] = temp;
}

我们发现,让数组a从下标1才开始排序是极其不合理的行为,所以,我们可以利用内存操作,新建一个数组b,让它的内存大小是n+1个int的大小,然后将a的数据从b+1的位置拷贝进b去,在排序后,将b数组的数据从b+1位置拷贝进a中,这时就实现了跟其他排序一样的排序表征了。

void heapSortNew(int a[],int n){
    int *b = (int *)malloc(sizeof(int) * (n+1));
    memset(b, 0, n+1);
    memcpy(b+1, a, sizeof(int) * n);
    buildHeap(b, n);
    for (int i = n; i > 0; i--) {
        swap(b, 1, i);
        heapify(b, 1, i-1);
    }
    memset(a, 0, n);
    memcpy(a, b+1, sizeof(int) * n);
    free(b);
}
  • 复杂度:
    • 时间复杂度
      • 平均:O(n log n)
      • 最好:O(n log n)
      • 最坏:O(n log n)
    • 空间复杂度 O(1)
  • 稳定:
    • 不稳定

6.归并排序

定义

归并排序(Merge Sort)就是利用归并的思想实现的排序算法。它的原理是假设初始序列有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到n/2个长度为2或1的有序子序列;再两两归并,...,如此重复,直到得到一个长度为n的有序序列为止,这种排序方法称为二路归并排序

二路归并排序

归并法其实就是使用分治法将数组两两分解,一直到只剩一个元素,然后再两两合并组合成有序数组。
两两数组比较合并为一个数组时,我们可以使用插入排序的办法,将右边数组一个个按照顺序插入到左边数组中,这样可以极大的优化排序

/*
 归并排序
 */
void merging(int *list_left,int list_left_size,int *list_right,int list_right_size);
/// 归并排序
/// @param a 数组
/// @param n 数组元素个数
void mergeSort(int a[],int n){
    if (n <= 1) {
        return;
    }
    //将数组等分为两段
    int *list_left = a;
    int list_left_size = n/2;
    int *list_right = a + n/2;
    int list_right_size = n - list_left_size;
    
    //分治法,不断递归,直到分为一个元素的数组,一个肯定是有序的
    mergeSort(list_left, list_left_size);
    mergeSort(list_right, list_right_size);
    
    merging(list_left, list_left_size, list_right, list_right_size);
}


/// 两个有序数组合并为一个有序数组  两个数组的元素一个个的相互比较来实现。
/// @param list_left 左数组
/// @param list_left_size 左边数组大小
/// @param list_right 右数组
/// @param list_right_size 右数组大小
//void merging(int *list_left,int list_left_size,int *list_right,int list_right_size){
//    int i,j,k,m;
//    i = j = k = m = 0;
//    //临时数组,用于存放比较后的数据
//    int temp[list_left_size+list_right_size];
//    //两个数组的元素相互比较,直到左右两边有一个数组的元素被取完
//    while (i < list_left_size && j < list_right_size) {
//        if (list_left[i] < list_right[j]) {
//            temp[k++] = list_left[i++];
//        } else {
//            temp[k++] = list_right[j++];
//        }
//    }
//    //如果左边还有剩,加到临时数组后面
//    while (i < list_left_size) {
//        temp[k++] = list_left[i++];
//    }
//    //如果是右边还有剩,加到临时数组后面
//    while (j < list_right_size) {
//        temp[k++] = list_right[j++];
//    }
//    //再把临时数组的数据加到原来那边
//    for (m = 0; m < (list_left_size + list_right_size); m++) {
//        list_left[m] = temp[m];
//    }
//}
/// 两个有序数组合并为一个有序数组  使用插入排序来实现,将list_right的数据插入左边(优化))
/// @param list_left 左数组
/// @param list_left_size 左边数组大小
/// @param list_right 右数组
/// @param list_right_size 右数组大小
void merging(int *list_left,int list_left_size,int *list_right,int list_right_size){
    //我们可以看做右边数组的每个元素需要在左边数组中找位置插入
    int i,j,leftMax,temp;//i表示右边数组的下标,j表示左边数组的下标
    
    //使用插入排序 优化排序
    for (i = 0; i < list_right_size; i++) {
        //leftMax是左边数组的最后一个值,也就是最大的值
        leftMax = list_left[list_left_size-1];
        if (list_right[i] < leftMax) {
            temp = list_right[i];
            for (j = list_left_size - 1; j >= 0 && list_left[j] > temp; j--) {
                list_left[j+1] = list_left[j];
            }
            list_left[j+1] = temp;
            list_left_size++;
        } else {
            //由于右边数组是有序的,所以这时候后面的都是已经有序了
            //而且由于数组是顺序存储结构,right是紧跟在left后面的
            //所以这时候数组a已经保证是n个连续区块是有序的
            //直接返回即可
            break;
        }
    }
    
}
  • 复杂度:
    • 时间复杂度
      • 平均:O(n log n)
      • 最好:O(n log n)
      • 最坏:O(n log n)
    • 空间复杂度 O(n)
  • 稳定:
    • 稳定

7.快速排序

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

解析:

  • 1.在数组中找一个值,作为分隔点,初始的时候将分隔点都是设置为low位置的值
  • 2.将小于分隔点的值扔在分割点的左边,将大于分割点的值扔在分割点的右边
  • 3.最后将分割点的值赋值给中间位置,然后继续对左右两边执行1和2步骤,知道分无可分

快速排序是对于大数组最好的排序方法,它是冒泡排序的升级版,代码实现如下:

void qSort(int a[],int low,int high);
int partition(int a[],int low,int high);
/// 快速排序
/// @param a 数组
/// @param n 数组元素个数
void quickSort(int a[],int n){
    qSort(a,0,n-1);
}
void qSort(int a[],int low,int high){
    //point表示一个分割点的下标
    int point;
    if (low < high) {
        //获取分割点,并将比分隔点元素小的放在分隔点左边,比分隔点大的放到分隔点右边
        point = partition(a,low,high);
        //递归排序分隔点左边
        qSort(a, low, point-1);
        //递归排序分隔点右边
        qSort(a, point+1, high);
    }
}
/// 将分隔点移动到中间,小于分隔点的元素放到分隔点左边,大于分隔点的元素放到分隔点右边
/// @param a 数组
/// @param low 低位
/// @param high 高位
int partition(int a[],int low,int high){
    //分隔点元素设置取低位的元素值
    int point = a[low];

    //这个循环最终将结束在low=high的时候
    while (low < high) {
        //从右边开始,循环找到比分割点小的元素位置
        while (low < high && a[high] >= point) {
            high--;//往左移动,那么high右边就是已经比分隔点大的
        }
        //在右边找到了比分隔点小的,交换位置,也就是放到分隔点的左边
        swap(a, low, high);
        //从左边开始,循环找到比分隔点大的元素位置
        while (low < high && a[low] <= point) {
            low++;//往右移动,那么low左边的就是已经比分隔点小的
        }
        //在左边找到了比分隔点大的元素,交换位置,也就是放到了分隔点的右边
        swap(a, low, high);
    }
    return low;
}
(1)、快速排序的优化:三数取中法

算法实现中我们可以发现,我们取分隔点一直都是取的a[low],而a[low]的值我们并不知道它是多大的,加入它是最大值或者最小值,那么我们就会多执行一次的排序,那么我们将它优化一下,取low、high、middle的值的中间值作为分隔点,这种优化方法叫做三数取中法

/*
 快排优化:三数取中法
 */
int partition(int a[],int low,int high){
    //分隔点元素使用三数取中法来获取
    //也就是在最前面、中间、最后面,三个数中取中间的值
    //中间位置
    int mid = low + (high-low)/2;
    //让前面的数是小的,后面的数是大的
    if (a[low] > a[high]) {
        swap(a, low, high);
    }
    //中间位置比最后的数值大,交换
    if (a[mid] > a[high]) {
        swap(a, mid, high);
    }
    //前面两个if保证了low和mid位置的元素都比high小
    //这时候,我们应该确保让low位置的元素为low和mid位置中较大的
    //也就是low位置元素要保证是中位数
    if (a[low] < a[mid]) {
        swap(a, low, mid);
    }
    int point = a[low];

    //这个循环最终将结束在low=high的时候
    while (low < high) {
        //从右边开始,循环找到比分割点小的元素位置
        while (low < high && a[high] >= point) {
            high--;//往左移动,那么high右边就是已经比分隔点大的
        }
        //在右边找到了比分隔点小的,交换位置,也就是放到分隔点的左边
        swap(a, low, high);
        //从左边开始,循环找到比分隔点大的元素位置
        while (low < high && a[low] <= point) {
            low++;//往右移动,那么low左边的就是已经比分隔点小的
        }
        //在左边找到了比分隔点大的元素,交换位置,也就是放到了分隔点的右边
        swap(a, low, high);
    }
    return low;
}
(2)、快速排序的优化: 优化掉不必要的交换

在将元素左右移动的时候,我们使用的是swap(a, low, high);,实际上,因为我们已经存储了point作为分割值,根本用不着进行交换,在循环中只用赋值即可,循环结束,再把中间位置的值设置为point即可

/*
 优化掉不必要的交换
 
 */
int partition(int a[],int low,int high){
    //分隔点元素使用三数取中法来获取
    //也就是在最前面、中间、最后面,三个数中取中间的值
    //中间位置
    int mid = low + (high-low)/2;
    //让前面的数是小的,后面的数是大的
    if (a[low] > a[high]) {
        swap(a, low, high);
    }
    //中间位置比最后的数值大,交换
    if (a[mid] > a[high]) {
        swap(a, mid, high);
    }
    //前面两个if保证了low和mid位置的元素都比high小
    //这时候,我们应该确保让low位置的元素为low和mid位置中较大的
    //也就是low位置元素要保证是中位数
    if (a[low] < a[mid]) {
        swap(a, low, mid);
    }
    int point = a[low];

    //这个循环最终将结束在low=high的时候
    while (low < high) {
        //从右边开始,循环找到比分割点小的元素位置
        while (low < high && a[high] >= point) {
            high--;//往左移动,那么high右边就是已经比分隔点大的
        }
        /*
         在右边找到了比分隔点小的,那么直接将它赋值到低位上
         在之前中,我们是进行交换,但是这个分隔点的值我们已经有point来存储了
         所以我们只用将值赋过去即可,根本用不着交换
        */
        a[low] = a[high];
        //从左边开始,循环找到比分隔点大的元素位置
        while (low < high && a[low] <= point) {
            low++;//往右移动,那么low左边的就是已经比分隔点小的
        }
        /*
         在左边找到了比分隔点大的元素,那么直接将它放到高位上
         在之前中,我们是进行交换,但是这个分隔点的值我们已经有point来存储了
         所以我们只用将值赋过去即可,根本用不着交换
         */
        a[high] = a[low];
    }
    //最后将中央的值赋值为分隔点的值即可
    a[low] = point;
    return low;
}

(3)、快速排序的优化:优化小数组时的排序方案

快速排序适合大数组时候的排序方式,而小数组的时候,直接插入排序是最好的排序方法
所以,我们设置一个数组长度值,大于这个值,我们使用快排,小于这个值,我们使用插入排序

/*
 优化小数组时的排序方案
 
 快速排序适合大数组时候的排序方式,而小数组的时候,直接插入排序是最好的排序方法
 所以,我们设置一个数组长度值,大于这个值,我们使用快排,小于这个值,我们使用插入排序
 */
#define MAX_LENGTH_INSERT_SORT 7
void qSort(int a[],int low,int high){
    //point表示一个分割点的下标
    int point;
    //如果大于7,使用快排
    if ((high-low) > MAX_LENGTH_INSERT_SORT) {
        //获取分割点,并将比分隔点元素小的放在分隔点左边,比分隔点大的放到分隔点右边
        point = partition(a,low,high);
        //递归排序分隔点左边
        qSort(a, low, point-1);
        //递归排序分隔点右边
        qSort(a, point+1, high);
    } else {
        //小于等于7,使用直接插入排序
        insertSort(a+low, high-low+1);
    }
}
(4)、快速排序的优化:优化递归操作,改为尾递归的形式

什么是尾递归呢?如果一个函数中递归形式的调用出现在函数的末尾,我们称这个递归函数是尾递归的。
当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活跃记录而不是在栈中取创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时,栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了。通过覆盖当前栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的执行效率会变得更高。
因此,只要有可能我们就需要将递归函数写成尾递归的形式。

#define MAX_LENGTH_INSERT_SORT 7
/*
 优化递归操作
 */
void qSort(int a[],int low,int high){
    //point表示一个分割点的下标
    int point;
    //如果大于7,使用快排
    if ((high-low) > MAX_LENGTH_INSERT_SORT) {
        
        while (low < high) {
            //获取分割点,并将比分隔点元素小的放在分隔点左边,比分隔点大的放到分隔点右边
            point = partition(a,low,high);
            
            //构建成尾递归
            if (point - low < high - point) {
                qSort(a, low, point-1);
                low = point + 1;
            } else {
                qSort(a, point+1, high);
                high = point - 1;
            }
        }
    } else {
        //小于等于7,使用直接插入排序
        InsertSort(a+low, high-low+1);
    }
}

(5)、全部优化后的完整算法
/*
 快速排序
 
 
 */
void qSort(int a[],int low,int high);
int partition(int a[],int low,int high);
/// 快速排序
/// @param a 数组
/// @param n 数组元素个数
void quickSort(int a[],int n){
    qSort(a,0,n-1);
}


/*
 优化掉不必要的交换
 
 */
int partition(int a[],int low,int high){
    //分隔点元素使用三数取中法来获取
    //也就是在最前面、中间、最后面,三个数中取中间的值
    //中间位置
    int mid = low + (high-low)/2;
    //让前面的数是小的,后面的数是大的
    if (a[low] > a[high]) {
        swap(a, low, high);
    }
    //中间位置比最后的数值大,交换
    if (a[mid] > a[high]) {
        swap(a, mid, high);
    }
    //前面两个if保证了low和mid位置的元素都比high小
    //这时候,我们应该确保让low位置的元素为low和mid位置中较大的
    //也就是low位置元素要保证是中位数
    if (a[low] < a[mid]) {
        swap(a, low, mid);
    }
    int point = a[low];

    //这个循环最终将结束在low=high的时候
    while (low < high) {
        //从右边开始,循环找到比分割点小的元素位置
        while (low < high && a[high] >= point) {
            high--;//往左移动,那么high右边就是已经比分隔点大的
        }
        /*
         在右边找到了比分隔点小的,那么直接将它赋值到低位上
         在之前中,我们是进行交换,但是这个分隔点的值我们已经有point来存储了
         所以我们只用将值赋过去即可,根本用不着交换
        */
        a[low] = a[high];
        //从左边开始,循环找到比分隔点大的元素位置
        while (low < high && a[low] <= point) {
            low++;//往右移动,那么low左边的就是已经比分隔点小的
        }
        /*
         在左边找到了比分隔点大的元素,那么直接将它放到高位上
         在之前中,我们是进行交换,但是这个分隔点的值我们已经有point来存储了
         所以我们只用将值赋过去即可,根本用不着交换
         */
        a[high] = a[low];
    }
    //最后将中央的值赋值为分隔点的值即可
    a[low] = point;
    return low;
}



#define MAX_LENGTH_INSERT_SORT 7
/*
 优化递归操作
 */
void qSort(int a[],int low,int high){
    //point表示一个分割点的下标
    int point;
    //如果大于7,使用快排
    if ((high-low) > MAX_LENGTH_INSERT_SORT) {
        
        while (low < high) {
            //获取分割点,并将比分隔点元素小的放在分隔点左边,比分隔点大的放到分隔点右边
            point = partition(a,low,high);
            
            //构建成尾递归
            if (point - low < high - point) {
                qSort(a, low, point-1);
                low = point + 1;
            } else {
                qSort(a, point+1, high);
                high = point - 1;
            }
        }
    } else {
        //小于等于7,使用直接插入排序
        InsertSort(a+low, high-low+1);
    }
}

快速排序的复杂度与稳定性
  • 复杂度:
    • 时间复杂度
      • 平均:O(n log n)
      • 最好:O(n log n)
      • 最坏:O(n^2)
    • 空间复杂度 O(log n) ~ O(n)
  • 稳定:
    • 不稳定

三、总结

从上面可以看出,排序算法可以分为两大类

  • 比较类排序
    通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
  • 非比较类排序
    不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。


    排序算法

各个排序算法的复杂度如下


算法复杂度

你可能感兴趣的:(十、排序算法)