logo资料库

各种排序算法的稳定性和时间复杂度总结.doc

第1页 / 共7页
第2页 / 共7页
第3页 / 共7页
第4页 / 共7页
第5页 / 共7页
第6页 / 共7页
第7页 / 共7页
资料共7页,全文预览结束
选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法, 冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。 冒泡法: 这是最原始,也是众所周知的最慢的算法了。他的名字的由来因为它的工作看来象是冒泡: 复 杂度为 O(n*n)。当数据为正序,将不会有交换。复杂度为 O(0)。 直接插入排序:O(n*n) 选择排序:O(n*n) 快速排序:平均时间复杂度 log2(n)*n,所有内部排序方法中最高好的,大多数情况下总是最好 的。 归并排序:log2(n)*n 堆排序:log2(n)*n 希尔排序:算法的复杂度为 n 的 1.2 次幂 这里我没有给出行为的分析,因为这个很简单,我们直接来分析算法: 首先我们考虑最理想的情况 1.数组的大小是 2 的幂,这样分下去始终可以被 2 整除。假设为 2 的 k 次方,即 k=log2(n)。 2.每次我们选择的值刚好是中间值,这样,数组才可以被等分。 第一层递归,循环 n 次,第二层循环 2*(n/2)...... 所以共有 n+2(n/2)+4(n/4)+...+n*(n/n) = n+n+n+...+n=k*n=log2(n)*n 所以算法复杂度为 O(log2(n)*n) 其他的情况只会比这种情况差,最差的情况是每次选择到的 middle 都是最小值或最大值,那么 他将变成交换法(由于使用了递归,情况更糟)。但是你认为这种情况发生的几率有多大??呵 呵,你完全不必担心这个问题。实践证明,大多数的情况,快速排序总是最好的。 如果你担心这个问题,你可以使用堆排序,这是一种稳定的 O(log2(n)*n)算法,但是通常情况下 速度要慢 于快速排序(因为要重组堆)。 这几天笔试了好几次了,连续碰到一个关于常见排序算法稳定性判别的问题,往往还是多选, 对于我以及和我一样拿不准的同学可不是一个能轻易下结论的题目,当然如果你笔试之前已经 记住了数据结构书上哪些是稳定的,哪些不是稳定的,做起来应该可以轻松搞定。 本文是针对老是记不住这个或者想真正明白到底为什么是稳定或者不稳定的人准备的。 首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前 2 个相等的数其
在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果 Ai = Aj, Ai 原来在位置前,排序后 Ai 还是要在 Aj 位置前。 其次,说一下稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另 一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位 排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排 序算法稳定,对基于比较的排序算法而言,元素交换的次数可能会少一些(个人感觉,没有证实)。 回到主题,现在分析一下常见的排序算法的稳定性,每个都给出简单的理由。 (1)冒泡排序 冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较, 交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交 换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这 时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。 (2)选择排序 选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元 素里面给第二个元素选择第二小的,依次类推,直到第 n-1 个元素,第 n 个元素不用选择了, 因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的 元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举 个例子,序列 5 8 5 2 9, 我们知道第一遍选择第 1 个元素 5 会和 2 交换,那么原序列中 2 个 5 的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。 (3)插入排序 插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有 序的小序列只有 1 个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入 的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到 找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相 等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后 的顺序,所以插入排序是稳定的。 (4)快速排序 快速排序有两个方向,左边的 i 下标一直往右走,当 a[i] <= a[center_index],其中 center_index 是中枢元素的数组下标,一般取为数组第 0 个元素。而右边的 j 下标一直往左走,当 a[j] > a[center_index]。如果 i 和 j 都走不动了,i <= j, 交换 a[i]和 a[j],重复上面的过程,直到 i>j。 交 换 a[j]和 a[center_index],完成一趟快速排序。在中枢元素和 a[j]交换的时候,很有可能把前面 的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素 5 和 3(第 5 个元素,下标 从 1 开始计)交换就会把元素 3 的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定 发生在中枢元素和 a[j]交换的时刻。 (5)归并排序 归并排序是把序列递归地分成短序列,递归出口是短序列只有 1 个元素(认为直接有序)或 者 2 个序列(1 次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直 到原序列全部排好序。可以发现,在 1 个或 2 个元素时,1 个元素不会交换,2 个元素如果大小 相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是
否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序 列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。 (6)基数排序 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最 高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次 序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别 收集,所以其是稳定的排序算法。 (7)希尔排序(shell) 希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大, 所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序 的序列效率很高。所以,希尔排序的时间复杂度会比 o(n^2)好一些。由于多次插入排序,我们 知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相 同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以 shell 排序是不稳定的。 (8)堆排序 我们知道堆的结构是节点 i 的孩子为 2*i 和 2*i+1 节点,大顶堆要求父节点大于等于其 2 个 子节点,小顶堆要求父节点小于等于其 2 个子节点。在一个长为 n 的序列,堆排序的过程是从 第 n/2 开始和其子节点共 3 个值选择最大(大顶堆)或者最小(小顶堆),这 3 个元素之间的选择当然 不会破坏稳定性。但当为 n/2-1, n/2-2, ...1 这些个父节点选择元素时,就会破坏稳定性。有可能 第 n/2 个父节点交换把后面一个元素交换过去了,而第 n/2-1 个父节点把后面一个相同的元素没 有交换,那么这 2 个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法 1 快速排序(QuickSort) 快速排序是一个就地排序,分而治之,大规模递归的算法。从本质上来说,它是归并排序的就 地版本。快速排序可以由下面四步组成。 (1) 如果不多于 1 个数据,直接返回。 (2) 一般选择序列最左边的值作为支点数据。 (3) 将序列分成 2 部分,一部分都大于支点数据,另外一部分都小于支点数据。 (4) 对两边利用递归排序数列。 快速排序比大部分排序算法都要快。尽管我们可以在某些特殊的情况下写出比快速排序快的算 法,但是就通常情况而言,没有比它更快的了。快速排序是递归的,对于内存非常有限的机器 来说,它不是一个好的选择。 2 归并排序(MergeSort) 归并排序先分解要排序的序列,从 1 分成 2,2 分成 4,依次分解,当分解到只有 1 个一组的时 候,就可以排序这些分组,然后依次合并回原来的序列中,这样就可以排序所有数据。合并排 序比堆排序稍微快一点,但是需要比堆排序多一倍的内存空间,因为它需要一个额外的数组。 3 堆排序(HeapSort)
堆排序适合于数据量非常大的场合(百万数据)。 堆排序不需要大量的递归或者多维的暂存数组。这对于数据量非常巨大的序列是合适的。比如 超过数百万条记录,因为快速排序,归并排序都使用递归来设计算法,在数据量非常大的时候, 可能会发生堆栈溢出错误。 堆排序会将所有的数据建成一个堆,最大的数据在堆顶,然后将堆顶数据和序列的最后一个数 据交换。接下来再次重建堆,交换数据,依次下去,就可以排序所有的数据。 4 Shell 排序(ShellSort) Shell 排序通过将数据分成不同的组,先对每一组进行排序,然后再对所有的元素进行一次插入 排序,以减少数据交换和移动的次数。平均效率是 O(nlogn)。其中分组的合理性会对算法产生 重要的影响。现在多用 D.E.Knuth 的分组方法。 Shell 排序比冒泡排序快 5 倍,比插入排序大致快 2 倍。Shell 排序比起 QuickSort,MergeSort, HeapSort 慢很多。但是它相对比较简单,它适合于数据量在 5000 以下并且速度并不是特别重要 的场合。它对于数据量较小的数列重复排序是非常好的。 5 插入排序(InsertSort) 插入排序通过把序列中的值插入一个已经排序好的序列中,直到该序列的结束。插入排序是对 冒泡排序的改进。它比冒泡排序快 2 倍。一般不用在数据大于 1000 的场合下使用插入排序,或 者重复排序超过 200 数据项的序列。 6 冒泡排序(BubbleSort) 冒泡排序是最慢的排序算法。在实际运用中它是效率最低的算法。它通过一趟又一趟地比较数 组中的每一个元素,使较大的数据下沉,较小的数据上升。它是 O(n^2)的算法。 7 交换排序(ExchangeSort)和选择排序(SelectSort) 这两种排序方法都是交换方法的排序算法,效率都是 O(n2)。在实际应用中处于和冒泡排序基 本相同的地位。它们只是排序算法发展的初级阶段,在实际中使用较少。 8 基数排序(RadixSort) 基数排序和通常的排序算法并不走同样的路线。它是一种比较新颖的算法,但是它只能用于整 数的排序,如果我们要把同样的办法运用到浮点数上,我们必须了解浮点数的存储格式,并通 过特殊的方式将浮点数映射到整数上,然后再映射回去,这是非常麻烦的事情,因此,它的使 用同样也不多。而且,最重要的是,这样算法也需要较多的存储空间。 9 总结 下面是一个总的表格,大致总结了我们常见的所有的排序算法的特点。 排序法 平均时间 最差情形 稳定度 额外空间 备注
O(n2) O(n2) 稳定 O(1) n 小时较好 冒泡 O(n2) 交换 选择 O(n2) O(n2) 不稳定 O(1) n 小时较好 插入 O(n2) O(n2) 稳定 O(1) 大部分已排序时较好 基数 O(logRB) O(logRB) 稳定 O(n) B 是真数(0-9), O(n2) 不稳定 O(1) n 小时较好 R 是基数(个十百) Shell O(nlogn) O(ns) 1(CMyData& data ); private: char* m_strDatamember; int m_iDataSize; }; //////////////////////////////////////////////////////// MyData.cpp 文件 //////////////////////////////////////////////////////// CMyData::CMyData(): m_iIndex(0), m_iDataSize(0), m_strDatamember(NULL) { } CMyData::~CMyData() { if(m_strDatamember != NULL) delete[] m_strDatamember;
m_strDatamember = NULL; } CMyData::CMyData(int Index,char* strData): m_iIndex(Index), m_iDataSize(0), m_strDatamember(NULL) { m_iDataSize = strlen(strData); m_strDatamember = new char[m_iDataSize+1]; strcpy(m_strDatamember,strData); CMyData& CMyData::operator =(CMyData &SrcData) { m_iIndex = SrcData.m_iIndex; m_iDataSize = SrcData.GetDataSize(); m_strDatamember = new char[m_iDataSize+1]; strcpy(m_strDatamember,SrcData.GetData()); return *this; while((pData[i]middle) && (j>left))//从右扫描大于中值的数 i++; j--; } } } bool CMyData::operator <(CMyData& data ) { return m_iIndex(CMyData& data ) { return m_iIndex>data.m_iIndex; } /////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////// //主程序部分 #include #include "MyData.h" template void run(T* pData,int left,int right) { int i,j; T middle,iTemp; i = left; j = right; //下面的比较都调用我们重载的操作符函数 middle = pData[(left+right)/2]; //求中间值 do{
if(i<=j)//找到了一对值 { //交换 iTemp = pData[i]; pData[i] = pData[j]; pData[j] = iTemp; i++; j--; } }while(i<=j);//如果两边扫描的下标交错,就停止(完成一次) //当左边部分有值(lefti),递归右半边 if(right>i) run(pData,i,right); template void QuickSort(T* pData,int Count) { run(pData,0,Count-1); void main() { } } } CMyData data[] = { CMyData(8,"xulion"), CMyData(7,"sanzoo"), CMyData(6,"wangjun"), CMyData(5,"VCKBASE"), CMyData(4,"jacky2000"), CMyData(3,"cwally"), CMyData(2,"VCUSER"), CMyData(1,"isdong") }; QuickSort(data,8); for (int i=0;i<8;i++) cout<<"\n"; cout<
分享到:
收藏