【转载】在线编程——排序算法总结

                                     在线编程——排序算法总结

【转载】原文链接:https://geekzw.blog.csdn.net/article/details/80419920

        找实习,阿里一面遇到手写快排,写出来感觉没错(VS2013能通过),但在阿里的测试平台上运行未通过。细思极恐,赶紧总结一波。有幸看到SteveWang的两篇博客:排序算法总结(1)排序算法总结(2)以及基数排序、计数排序与桶排序,总结的相当详细,我这里算是重新拜读一遍,结合自己的理解写下来。

一、排序算法分类,稳定性分析,时间复杂度与空间复杂度总结表

       我们通常所说的排序算法往往指的是内部排序算法,即数据记录在内存中进行排序。而外部排序是指大文件的排序,即待排序的记录存储在外存储器上,在排序过程中需要进行多次的内、外存之间的交换。

      (一)内部排序算法大体可分为两大类:

        1、比较排序,平均时间复杂度范围:O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。

然而,参考博客:https://blog.csdn.net/zlele0326/article/details/51281578,比较排序算法可以继续分为下面几类:

(1)插入排序:直接插入排序,二分法插入排序,希尔排序;

(2)选择排序:简单选择排序,堆排序;

(3)交换排序:冒泡排序,快速排序;

(4)归并排序;

  2、非比较排序,时间复杂度可以达到O(n),主要有:计数排序基数排序桶排序等。详见地址:戳我

(二)稳定性分析

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

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

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

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

(三)排序算法总结表

      尽管记了很多次几个排序算法的时间复杂度与空间复杂度,日子一长又忘了,心累,各种排序的稳定性,时间复杂度、空间复杂度、稳定性总结如下图(参考:https://blog.csdn.net/foreverling/article/details/43798223):

                                                                                                              图1

                                                                                                               图2

        网上现主要有两个版本的排序表格,如上图1,图2(来自《大话数据结构》)所示。可以看到两张图中希尔排序的时间复杂度不同,以及快速排序的空间复杂度不同,那究竟是什么?

(1)希尔排序时间复杂度分析:(摘自百度百科),希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n^2),而Hibbard增量的希尔排序时间复杂度为O(n^3/2),希尔排序时间复杂度的下界是n*log2n。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择,但是比O( n^2 )复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法. 希尔排序算法的性能与所选取的分组长度序列有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数。想要弄清关键词比较次数和记录移动次数与增量选择之间的关系,并给出完整的数学分析,至今仍然是数学难题。

(2)快速排序空间复杂度分析:详见博客(https://blog.csdn.net/yuzhihui_no1/article/details/44198701),首先就地快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据,即快速排序的空间复杂度为:O(logn) ~O( n ) 。

最优的情况下空间复杂度为:O(logn)  ;每一次都平分数组的情况

最差的情况下空间复杂度为:O( n )      ;退化为冒泡排序的情况

二、比较排序

1、直接插入排序

插入排序是一种简单直观的排序算法。它的工作原理非常类似于我们抓扑克牌

        对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

  具体算法描述如下:

       (1) 从第一个元素开始,该元素可以认为已经被排序;

       (2) 取出下一个元素,在已经排序的元素序列中从后向前扫描;

       (3) 如果该元素(已排序)大于新元素,将该元素移到下一位置;

       (4) 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;

       (5) 将新元素插入到该位置后;

       (6) 重复步骤2~5;

C++代码为:


   
   
  1. #include <iostream>
  2. #include <vector>
  3. using namespace std;
  4. // 分类 ------------- 内部比较排序
  5. // 数据结构 ---------- 向量
  6. // 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
  7. // 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
  8. // 平均时间复杂度 ---- O(n^2)
  9. // 所需辅助空间 ------ O(1)
  10. // 稳定性 ------------ 稳定
  11. vector <int> InsertionSort(vector <int> A)
  12. {
  13. for (int i = 1; i < A.size(); i++) // 类似抓扑克牌排序
  14. {
  15. int get = A[i]; // 右手抓到一张扑克牌
  16. int j = i - 1; // 拿在左手上的牌总是排序好的
  17. while (j >= 0 && A[j] > get) // 将抓到的牌与手牌从右向左进行比较
  18. {
  19. A[j + 1] = A[j]; // 如果该手牌比抓到的牌大,就将其右移
  20. j--;
  21. }
  22. A[j + 1] = get; // 直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
  23. }
  24. return A;
  25. }
  26. int main()
  27. {
  28. //输入不定长的待排序列
  29. vector <int> vec;
  30. int temp;
  31. char t;
  32. cout << "输入待排序列为:";
  33. do{
  34. cin >> temp;
  35. vec.push_back(temp);
  36. } while ((t=cin.get())!='\n');
  37. /
  38. int n = vec.size();//待排序列的长度 ,若输入为数组A,则int n = sizeof(A) / sizeof(int);
  39. cout << "排序前的结果为:";
  40. for (int i = 0; i < n; i++)
  41. {
  42. cout << vec[i] << " ";
  43. }
  44. cout << endl;
  45. /
  46. vector<int> res = InsertionSort(vec); //调用直接插入排序算法
  47. cout << "排序后的结果为:";
  48. for (int i = 0; i < n; i++)
  49. {
  50. cout << res[i] << " ";
  51. }
  52. cout << endl;
  53. system("pause");
  54. return 0;
  55. }

运行结果:

插入排序过程:动态图来自http://www.cnblogs.com/eniac12/p/5329396.html#3972917

                                 

        插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

python代码如下:


   
   
  1. def InsertSort(myList):
  2. length = len(myList)
  3. for i in range(1,length):
  4. # 设置当前值前一个元素的标识
  5. j = i - 1
  6. #如果当前值小于前一个元素,则将当前值作为一个临时变量存储,将前一个元素后移一位
  7. if(myList[i] < myList[j]):
  8. temp = myList[i]
  9. myList[ i] = myList[j]
  10. # 继续往前寻找,如果有比临时变量大的数字,则后移一位,直到找到比临时变量小的元素或者达到列表第一个元素
  11. j = j-1
  12. while j>=0 and myList[j] > temp:
  13. myList[j+1] = myList[j]
  14. j = j-1
  15. # 将临时变量赋值给合适位置
  16. myList[j+1] = temp
  17. if __name__=="__main__":
  18. arr = [int(i) for i in input().split()] # 输入待排序列
  19. InsertSort(arr) # 插入排序
  20. print(" ".join(str(i) for i in arr)) # 输出排序结果

运行结果:

 

2、快速排序算法

    快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。

  快速排序使用分治策略把一个序列分为两个子序列。步骤为:

(1)从序列中挑出一个元素,作为"基准"(pivot).

(2)把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。

(3)对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。

C++代码:


   
   
  1. #include <iostream>
  2. #include <vector>
  3. #include <exception>
  4. using namespace std;
  5. // 分类 ------------ 内部比较排序
  6. // 数据结构 --------- 向量
  7. // 最差时间复杂度 ---- 每次选取的基准都是最大(或最小)的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
  8. // 最优时间复杂度 ---- 每次选取的基准都是中位数,这样每次都均匀的划分出两个分区,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
  9. // 平均时间复杂度 ---- O(nlogn)
  10. // 所需辅助空间 ------ 主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)
  11. // 稳定性 ---------- 不稳定
  12. void Swap(vector<int>& data, int i, int j)
  13. {
  14. int temp = data[i];
  15. data[i] = data[j];
  16. data[j] = temp;
  17. }
  18. int RandomInRange(int min, int max) //在min~max中随机生成一个值
  19. {
  20. int random = rand() % (max - min + 1) + min;
  21. return random;
  22. }
  23. int Partition(vector<int>& data, int start, int end) //Partition函数
  24. {
  25. if (data.size() <= 0 || start < 0 || end >= data.size())
  26. throw new std::exception( "Invalid Parameters");
  27. //int index = RandomInRange(start, end); //可以不调用RandomInRange函数
  28. //Swap(data, index, end);
  29. int small = start - 1;
  30. for ( int index = start; index < end; ++index)
  31. {
  32. if (data[index] < data[end])
  33. {
  34. ++small;
  35. if (small != index)
  36. Swap(data, index, small);
  37. }
  38. }
  39. ++small;
  40. Swap(data, small, end);
  41. return small;
  42. }
  43. //递归快速排序
  44. void QuickSort(vector<int>& data, int start, int end)
  45. {
  46. if (start >= end)
  47. return;
  48. int index = Partition(data, start, end); //基准的索引
  49. QuickSort(data, start, index - 1);
  50. QuickSort(data, index + 1, end);
  51. }
  52. int main()
  53. {
  54. //输入不定长的待排序列
  55. vector< int> vec;
  56. int temp;
  57. char t;
  58. cout << "输入待排序列为:";
  59. do{
  60. cin >> temp;
  61. vec.push_back(temp);
  62. } while ((t = cin.get()) != '\n');
  63. /
  64. int n = vec.size(); //待排序列的长度 ,若输入为数组A,则int n = sizeof(A) / sizeof(int);
  65. cout << "排序前的结果为:";
  66. for ( int i = 0; i < n; i++)
  67. {
  68. cout << vec[i] << " ";
  69. }
  70. cout << endl;
  71. /
  72. QuickSort(vec, 0, n - 1); //调用快速排序算法
  73. /
  74. cout << "排序后的结果为:";
  75. for ( int i = 0; i < n; i++)
  76. {
  77. cout << vec[i] << " ";
  78. }
  79. cout << endl;
  80. system( "pause");
  81. return 0;
  82. }

运行结果:

快速排序过程展示:

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

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

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

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

       需要强调的是,很多公司的面试官喜欢在面试环节要求应聘者写出快速排序的代码,剑指offer上一个比较好的方法,保存了数组的两个位置index向前遍历数组,small用于保存交换的小于轴值的数,找到一个前移一步,即上面的函数:


    
    
  1. void Swap(vector <int> &data, int i, int j)
  2. {
  3. int temp = data[i];
  4. data[i] = data[j];
  5. data[j] = temp;
  6. }
  7. int RandomInRange(int min, int max) //在min~max中随机生成一个值
  8. {
  9. int random = rand() % (max - min + 1) + min;
  10. return random;
  11. }
  12. int Partition(vector <int> &data, int start, int end) //Partition函数
  13. {
  14. if (data.size() <= 0 || start < 0 || end >= data.size())
  15. throw new std::exception("Invalid Parameters");
  16. //int index = RandomInRange(start, end);
  17. //Swap(data, index, end);
  18. int small = start - 1;
  19. for (int index = start; index < end; ++index)
  20. {
  21. if (data[index] < data[end])
  22. {
  23. ++small;
  24. if (small != index)
  25. Swap(data, index, small);
  26. }
  27. }
  28. ++small;
  29. Swap(data, small, end);
  30. return small;
  31. }
  32. //递归快速排序
  33. void QuickSort(vector<int> &data, int start, int end)
  34. {
  35. if (start >= end)
  36. return;
  37. int index = Partition(data, start, end); //基准的索引
  38. QuickSort(data, start, index - 1);
  39. QuickSort(data, index + 1, end);
  40. }

        现在回答,为什么在阿里笔试过程中,自己写出来的快排,感觉没错,但是运行结果不对。主要原因:
void QuickSort(vector<int>& data, int start, int end) //快速排序
    
    
int Partition(vector<int>& data, int start, int end) //Partition函数
   
   

写成了

void QuickSort(vector<int> data, int start, int end) //快速排序
   
   
int Partition(vector<int> data, int start, int end) //Partition函数
   
   

运行结果:

本质原因为:vector的传参方式理解不透彻,详见https://www.cnblogs.com/xiaoxi666/p/6843211.html

为了在面试过程中快速写出代码,如果允许写python代码,则为:


   
   
  1. # 快速排序
  2. def quick_sort(array, l, r):
  3. if l < r:
  4. q = partition(array, l, r)
  5. quick_sort( array, l, q - 1)
  6. quick_sort( array, q + 1, r)
  7. def partition( array, l, r) :
  8. x = array[r]
  9. i = l - 1
  10. for j in range( l, r) :
  11. if array[ j] <= x:
  12. i += 1
  13. array[ i], array[ j] = array[j], array[ i]
  14. array[ i + 1], array[ r] = array[r], array[ i + 1]
  15. return i + 1
  16. if __name__== "__main__" :
  17. arr = [int(i) for i in input() .split()] # 输入待排序列
  18. quick_sort( arr, 0, len( arr) -1) # 快速排序
  19. print(" " .join( str( i) for i in arr)) # 输出排序结果

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值