zoukankan      html  css  js  c++  java
  • 快速排序的介绍以及其两大坑点

    前言


      学完冒泡排序之后,我们会发现冒泡排序虽然比桶排序的空间效率要高,但是冒泡排序的执行效率却是很低的,它的时间复杂度达到O(N^2)。假设电脑一秒可运行10亿次,那么对一亿个数进行排序的话,桶排序需要0.1秒,而冒泡排序需要1千万秒,相当于115天,是不是很可怕?有没有什么双赢的算法呢?有的,就是快速排序。

      

    步骤


      假设给出一个数列 6 1 2 7 9 3 4 5 10 8,输出 1 2 3 4 5 6 7 8 9 10。我们利用快速排序的解法去做

      1.设一个基准数(也称中枢元素,起对照作用),譬如设头元素6

      2.把比基准数小的都排在左边,把比基准数大的都排在右边,譬如3 1 2 5 4 6 9 7 10 8 

      3.对左边的数列 3 1 2 5 4 再进行步骤 1 步骤 2 的操作

      4.子数列排序成了 2 1 3 5 4

      5.再对它的子数列进行步骤 1 2 的操作

      6.最终将得到10个单元素数列 1  , 2 , 3, 4, 5, 6, 7, 8, 9, 10

      

    第一轮快排的卡通版过程图如下(源于《啊哈!算法》):

     

      

      

      

      

      

      

     

    全部的流程图如下:

      

     代码实现


    void quickSort (int *a, int left, int right) {
        int centerIndex;
        int i, j, temp;
        centerIndex = a[left];
        i = left;
        j = right;
        if (i >= j) {return;} // prevent the ocurrence "left > j-1" 
        while (i < j) {
            while ((a[j] >= centerIndex) && (i < j)) { // j is first 
                --j;
            }
            while ((a[i] <= centerIndex) && (i < j)) {
                ++i;
            }
            temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
        temp =  a[j]; //exchange real centerIndex
        a[j] = a[left];
        a[left] = temp;
        quickSort(a, left, j-1);
        quickSort(a, j+1, right);
    }

     

    两大坑点


      第一个坑点:每次扫描时必须先保证哨兵 j 先动,然后哨兵 i 再动,而且在扫描时时刻保持 i<j 。

      这个设计思路是为了应对当子序列已经是排序好的情况,此时只能交换的数字只能是中枢元素本本身,不应把基准数与其他数字再交换而破坏了原子序列。举个例子,当子序列为 1 2 3 4 ,设基准数是 1 ,如果此时i 先走,最终 i  = 1, j = 1,a[i] = 2,a[j] = 2,交换基准数与 a[j] ,子序列变为 2 1 3 4 ,原子序列被破坏。

      第二个坑点是递归的出口需考虑哨兵 j 越界的情况,结束递归的条件是 left > =right 而不是 left == right。

      我一开始也认为递归出口设置为 left == right 就万事大吉了,因为它可以表示当前子序列只有一个元素,那么就不用进行快排了。但是漏考虑了在子序列已经排序好的情况时 进行扫描的话,会出现 j - 1 < left 的越界问题。举个例子,当子序列为 3 7 9 6 ,基准数是 3 ,最终经过扫描后 j = 0,a[j] = 0,此时 (j-1) = -1,你将把 j-1 作为 right 传入函数中,而此时  left = 0,下次的分治会出现 left > right ,出现逻辑问题。

    时间复杂度以及空间复杂度分析


      平均情况下,划分需要时间 O(n),而递归深度O(lgn) ,所以时间复杂度是 O(nlgn),最坏情况是输入有序串,导致 O(n^2) ,如果采用随机化算法,时间复杂度为O(nlgn)

       由于需要递归调用,所以空间复杂度是 O(lgn)

    总结


      总的来说,快排的操作是这样的:快速排序有两个方向,当 a[i] <= a[center_index],左边的i下标一直往右走,其中 center_index 是中枢元素的数组下标,一般取为数组第0个元素。而当 a[j] > a[center_index],右边的j下标一直往左走。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。交换 a[j] 和 a[center_index],完成一趟快速排序。

      快速排序办法之所以快速,是因为相比于冒泡排序,每次交换都是跳跃式的。每次排序的时候都设置一个基准点,小的放左边,大的放右边,这样就不会像冒泡排序那样只在相邻的数之间进行交换,交换的距离就大得多了。当然,在最坏的情况下,仍可能是相邻的两个数进行交换。因此,快排的最差时间复杂度仍是O(N^2),它的平均时间复杂度为O(NlogN)。这种方法基于“二分”思想,涉及到了分治法(不断分割成子数列,然后分别处理)

     

    ————全心全意投入,拒绝画地为牢
  • 相关阅读:
    那是什么进程 —— svchost.exe是什么? 它为何运行?
    共享一下我的博客皮肤
    C#3.0亮点 —— 关键字var和匿名类型
    改善代码设计 —— 优化物件之间的特性(Moving Features Between Objects)
    C#3.0亮点 —— 分部方法
    解决C#中一个"异步方法却同步执行"的问题
    改善代码设计 —— 简化函数调用(Making Method Calls Simpler)
    改善代码设计 —— 总结篇(Summary)
    理解A*寻路算法具体过程
    改善代码设计 —— 组织好你的数据(Composing Data)
  • 原文地址:https://www.cnblogs.com/Bw98blogs/p/7172289.html
Copyright © 2011-2022 走看看