1.冒泡排序
2.选择排序
3.插入排序
4.交换排序
5.归并排序
6.快速排序
7.堆排序
8.希尔排序
9.双向冒泡排序
10.计数排序
11.基数排序
一、简单排序算法
1.冒泡法:
这是最原始,也是众所周知的最慢的算法了。他的名字的由来因为它的工作看来象是冒泡:
// 冒泡法排序(升序):O(n^2) // 思想:倒序,把比前一个元素小的与前一个交换,就相当于气泡上升。 void BubbleSort(int* pData,int Count) { int iTemp; for(int i=1; i < Count; i++) // i,气泡上升顶点, { for(int j = Count-1; j >= i; j--) // 气泡上升过程 { if(pData[j]<pData[j-1]) { iTemp = pData[j-1]; pData[j-1] = pData[j]; pData[j] = iTemp; } } } }
倒序(最糟情况)
第一轮:10,9,8,7->10,9,7,8->10,7,9,8->7,10,9,8(交换3次)
第二轮:7,10,9,8->7,10,8,9->7,8,10,9(交换2次)
第一轮:7,8,10,9->7,8,9,10(交换1次)
循环次数:6次
交换次数:6次
其他:
第一轮:8,10,7,9->8,10,7,9->8,7,10,9->7,8,10,9(交换2次)
第二轮:7,8,10,9->7,8,10,9->7,8,10,9(交换0次)
第一轮:7,8,10,9->7,8,9,10(交换1次)
循环次数:6次
交换次数:3次
上面我们给出了程序段,现在我们分析它:这里,影响我们算法性能的主要部分是循环和交换,
显然,次数越多,性能就越差。从上面的程序我们可以看出循环的次数是固定的,为1+2+...+n-1。
写成公式就是1/2*(n-1)*n。
现在注意,我们给出O方法的定义:
若存在一常量K和起点n0,使当n>=n0时,有f(n)<=K*g(n),则f(n) = O(g(n))。
从程序后面所跟的表可以看到,两种情况的循环相同,交换不同。其实交换本身同数据源的
有序程度有极大的关系,当数据处于倒序的情况时,交换次数同循环一样(每次循环判断都会交换),
复杂度为O(n*n)。当数据为正序,将不会有交换。复杂度为O(0)。乱序时处于中间状态。正是由于这样的
原因,我们通常都是通过循环次数来对比算法。
2.交换法:
交换法的程序最清晰简单,每次用当前的元素一一的同其后的元素比较并交换。
// 交换法排序(升序):O(n^2) // 思想:对于每一个元素,如果其后面的元素比它小,立刻与之交换,继续与之后的元素比较,交换 void ExchangeSort(int* pData,int Count) { int iTemp; for(int i=0;i<Count-1;i++) { for(int j=i+1;j<Count;j++) { if(pData[j]<pData[i]) { iTemp = pData[i]; pData[i] = pData[j]; pData[j] = iTemp; } } } }
倒序(最糟情况)
第一轮:10,9,8,7->9,10,8,7->8,10,9,7->7,10,9,8(交换3次)
第二轮:7,10,9,8->7,9,10,8->7,8,10,9(交换2次)
第一轮:7,8,10,9->7,8,9,10(交换1次)
循环次数:6次
交换次数:6次
其他:
第一轮:8,10,7,9->8,10,7,9->7,10,8,9->7,10,8,9(交换1次)
第二轮:7,10,8,9->7,8,10,9->7,8,10,9(交换1次)
第一轮:7,8,10,9->7,8,9,10(交换1次)
循环次数:6次
交换次数:3次
从运行的表格来看,交换几乎和冒泡一样糟。事实确实如此。循环次数和冒泡一样
也是1/2*(n-1)*n,所以算法的复杂度仍然是O(n*n)。由于我们无法给出所有的情况,所以
只能直接告诉大家他们在交换上面也是一样的糟糕(在某些情况下稍好,在某些情况下稍差)。
3.选择法:
现在我们终于可以看到一点希望:选择法,这种方法提高了一点性能(某些情况下)
这种方法类似我们人为的排序习惯:从数据中选择最小的同第一个值交换,在从剩下的部分中
选择最小的与第二个交换,这样往复下去。
// 选择法排序(升序):O(n^2) // 思想:从前往后,对于每一个元素,均从其与之后所有元素中选择最小的一个交换这个元素 void SelectSort(int* pData,int Count) { int iTemp; int iPos; for(int i=0;i<Count-1;i++) { iTemp = pData[i]; iPos = i; for(int j=i+1;j<Count;j++) // 从其后找最小的一个 { if(pData[j]<iTemp) { iTemp = pData[j]; iPos = j; } } pData[iPos] = pData[i]; // 交换最小的元素 pData[i] = iTemp; } }
倒序(最糟情况)
第一轮:10,9,8,7->(iTemp=9)10,9,8,7->(iTemp=8)10,9,8,7->(iTemp=7)7,9,8,10(交换1次)
第二轮:7,9,8,10->7,9,8,10(iTemp=8)->(iTemp=8)7,8,9,10(交换1次)
第一轮:7,8,9,10->(iTemp=9)7,8,9,10(交换0次)
循环次数:6次
交换次数:2次
其他:
第一轮:8,10,7,9->(iTemp=8)8,10,7,9->(iTemp=7)8,10,7,9->(iTemp=7)7,10,8,9(交换1次)
第二轮:7,10,8,9->(iTemp=8)7,10,8,9->(iTemp=8)7,8,10,9(交换1次)
第一轮:7,8,10,9->(iTemp=9)7,8,9,10(交换1次)
循环次数:6次
交换次数:3次
遗憾的是算法需要的循环次数依然是1/2*(n-1)*n。
所以算法复杂度为O(n*n)。
我们来看他的交换。由于每次外层循环只产生一次交换(只有一个最小值)。所以f(n)<=n
所以我们有f(n)=O(n)。所以,在数据较乱的时候,可以减少一定的交换次数。
4.插入法:
插入法较为复杂,它的基本工作原理是抽出牌,在前面的牌中寻找相应的位置插入,然后继续下一张
// 插入法排序:O(n^2) // 思想:把数组分为有序区和无序区两个部分,初始以起始第一个元素为有序区 // 每次取出无序区的第一个元素,相当于把有序区容量加1,把取出的元素与有序区元素倒序比较 // 如果取出的元素较小,则把该有序区元素后移,同时继续比较,直到其不小于有序区该位置元素 // 把取出的元素,写入到该位置的下一位。 // 由于空位一直随着比较迁移,不要再做其他移动工作。 // 就相当于码扑克牌,从排队里抓起一张,然后插入到合适的位置,同时也把比它大的牌后移。 void InsertSort(int* pData,int Count) { int iTemp; int iPos; for(int i=1;i<Count;i++) { iTemp = pData[i]; iPos = i-1; while((iPos>=0) && (iTemp<pData[iPos])) // 找寻要插入的位置 { pData[iPos+1] = pData[iPos]; // 要插入的位置还在前面,要把当前元素后移,留出空位 iPos--; } pData[iPos+1] = iTemp; // 插入 } }
倒序(最糟情况)
第一轮:10,9,8,7->9,10,8,7(交换1次)(循环1次)
第二轮:9,10,8,7->8,9,10,7(交换1次)(循环2次)
第一轮:8,9,10,7->7,8,9,10(交换1次)(循环3次)
循环次数:6次
交换次数:3次
其他:
第一轮:8,10,7,9->8,10,7,9(交换0次)(循环1次)
第二轮:8,10,7,9->7,8,10,9(交换1次)(循环2次)
第一轮:7,8,10,9->7,8,9,10(交换1次)(循环1次)
循环次数:4次
交换次数:2次
上面结尾的行为分析事实上造成了一种假象,让我们认为这种算法是简单算法中最好的,其实不是,
因为其循环次数虽然并不固定,我们仍可以使用O方法。从上面的结果可以看出,循环的次数f(n)<=
1/2*n*(n-1)<=1/2*n*n。所以其复杂度仍为O(n*n)(这里说明一下,其实如果不是为了展示这些简单
排序的不同,交换次数仍然可以这样推导)。现在看交换,从外观上看,交换次数是O(n)(推导类似
选择法),但我们每次要进行与内层循环相同次数的‘=’操作。正常的一次交换我们需要三次‘=’
而这里显然多了一些,所以我们浪费了时间。
二、高级排序算法:
5、归并排序:
归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
归并排序具体工作原理如下(假设序列共有n个元素):
// 归并排序--归并函数 void Merge(int* pData, int first, int mid, int last) { //申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列 int *pTemp = new int[last-first+1]; int begin1 = first, begin2 = mid + 1; int i; for(i = 0; begin1 <= mid && begin2 <= last; ++i) { //比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置 if(pData[begin1] <= pData[begin2]) pTemp[i] = pData[begin1++]; else pTemp[i] = pData[begin2++]; } //若有剩余,直接拷贝出来粘到合并序列尾 if(begin1 <= mid) memcpy_s(pTemp + i, (last-first+1)*sizeof(int), pData + begin1, (mid-begin1+1)*sizeof(int)); else if(begin2 <= last) memcpy_s(pTemp + i, (last-first+1)*sizeof(int), pData + begin2, (last-begin2+1)*sizeof(int)); //将排序好的序列拷贝回数组中 memcpy_s(pData + first, (last-first+1)*sizeof(int), pTemp, (last-first+1)*sizeof(int)); delete pTemp; } // 归并排序 // 思路:分组排序,把排序后的数组归并 void MergeSort(int* pData, int first, int last) { int mid = 0; if(first < last) { mid = (first+last)/2; MergeSort(pData, first, mid); MergeSort(pData, mid+1, last); Merge(pData, first, mid, last); } }
比较操作的次数介于(nlogn) / 2和nlogn − n + 1。 赋值操作的次数是(2nlogn)。 归并算法的空间复杂度为:Θ (n)
6、快速排序:
首先我们选择一个中间值middle程序中我们使用数组中间值,然后
把比它小的放在左边,大的放在右边(具体的实现是从两边找,找到一对后交换)。然后对两边分别使
用这个过程(最容易的方法——递归)。
#include <utility> // 快速排序,递归算法 // 思路:取数组中间值作为划分元素,使该值左端元素均不大于它,右端元素均不小于它。该值在排序后的位置就定了。 void QuickSort1(int* pData, int left, int right) // left和right表示要排序的范围 { int i,j; int middle,iTemp; i = left; j = right; middle = pData[(left+right)/2]; //求中间值 ,作为划分元素 do { while((pData[i]<middle) && (i<right)) //从左扫描大于中值的数 i++; while((pData[j]>middle) && (j>left)) //从右扫描大于中值的数 j--; if(i<=j) //找到了一对值 { //交换,使左端元素均不大于划分元素,右端元素均不小于划分元素 std::swap(pData[i], pData[j]); i++; j--; } }while(i<=j);//如果两边扫描的下标交错,就停止(完成一次) //当左边部分有值(left<j),递归左半边 if(left<j) QuickSort1(pData,left,j); //当右边部分有值(right>i),递归右半边 if(right>i) QuickSort1(pData,i,right); } // 快速排序,非递归算法 // 思路:同上 // 优化:1、当划分组元素个数较少时,直接使用插入法排序,提高效率。 // 2、选择左、中、右三个元素中处于中间的那个元素为划分元素,防止仅选择中间元素作为换分元素时,中间元素恰好为最小或最大值的情况。 void QSort(int* pData, int nCount, int m = -1) { static const int M=7, NSTACK=64; // M是使用直接插入法排序的子数组大小,NSTACK是所需的辅助存储器 int i,ir,j,jstack=-1,l=0; // i,j为循环算子,ir为当前分组右端,l当前为分组左端,jstack为当前栈数组滑标 int a; int istack[NSTACK]; // 堆栈,存放分组范围,i存放分组范围左端,i+1存放分组范围右端 if (m > 0) nCount = min(m,nCount); // m是可选参数,如果m>0,标识只对前m个元素排序 ir=nCount-1; // 初始分组,ir为最右端位置,l为起始点位置 for (;;) { if (ir-l < M) // 当子数组足够小时,插入法排序 { for (j=l+1;j<=ir;j++) // 插入法排序算法 { a=pData[j]; for (i=j-1;i>=l;i--) { if (pData[i] <= a) break; pData[i+1]=pData[i]; } pData[i+1]=a; } if (jstack < 0) break; // 堆栈为NULL,数组已经排序完成 // 弹出堆栈,并开始新一轮划分 ir=istack[jstack--]; // 右端 l=istack[jstack--]; // 左端 } else // 快速排序核心算法 { int k=(l+ir) >> 1; // k为当前分组范围的中点 std::swap(pData[k],pData[l+1]); // 先把中间元素存放在第二位置 // 挑选左、中、右三元素的中值作为划分元素a // 同时重新排列以使a[l]<=a[l+1]<=a[ir] if (pData[l] > pData[ir]) { std::swap(pData[l],pData[ir]); } if (pData[l+1] > pData[ir]) { std::swap(pData[l+1],pData[ir]); } if (pData[l] > pData[l+1]) { std::swap(pData[l],pData[l+1]); } i=l+1; // 为划分初始化指针,左指针 j=ir; // 右指针 a=pData[l+1]; // 取划分元素 for (;;) // 开始内循环 { do i++; while (pData[i] < a); // 从数组首部向后寻找大于a的元素 do j--; while (pData[j] > a); // 从数组尾部向前寻找小于a的元素 if (j < i) break; // 指针交汇,划分结束 std::swap(pData[i],pData[j]); // 各自找到元素,交换元素 } // 结束内循环,此时j左端的均元素均小于j右端的元素,且j位置的元素小于当前划分元素a. pData[l+1]=pData[j]; // 插入划分元素,此时该元素位置已定 pData[j]=a; // 准备入栈,此时各指针顺序:[l...ji...ir],j处元素就是以排好序的划分元素a jstack += 2; // 将指针压入堆栈中更大的数组,立刻处理较小的数组 if (jstack >= NSTACK) throw("NSTACK too small in sort."); if (ir-i+1 >= j-l) // 右端较长,压入右端部分 { istack[jstack]=ir; istack[jstack-1]=i; ir=j-1; // 修改右端指针 } else // 压入左端部分入栈 { istack[jstack]=j-1; istack[jstack-1]=l; l=i; // 修改左端指针 } } } }
这里我没有给出行为的分析,因为这个很简单,我们直接来分析算法:首先我们考虑最理想的情况
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)算法,但是通常情况下速度要慢
于快速排序(因为要重组堆)。
7、堆排序
堆排序的完整实现。代表了一个大公司的整个生存循环:雇佣N/2个工人,雇佣N/2个潜在的管理者,有一个值为升迁的过程,它是一种超级Peter原理的排序;最后按注定的过程:每个原来的雇员被一次提升为总裁。
堆排序的过程是:
shift_down
,目的是把新的数组顶端数据调整到相应位置,即重建堆
// 元素提升(上移),从[l,r]的元素中提出最大值作为堆[l,r]的堆顶。 // 创建堆时:假设堆的形式是公司组织的二叉树结构,首先有N/2个元素作为员工,然后每进入一个元素作为潜在管理者, // 选择两个员工作为其直属下级,并从下级中选择较大值作为这两个员工的上级 // 并把新来的员工插入到被升级的员工的位置上去,直至进入N/4个员工,选出N/4个上层节点; // 然后再进入节点时,就作为更高层次的潜在管理者,需要从下一级中找寻较大的作为此位置的真正管理者,如果下级的要升级,要带动更下级的升级 // 重建堆时:每次从堆顶的直接下一级中选择最大元素晋升为老板,同时带动更下级的升迁。 // 堆的特点:二叉树的形式,根为最大值(大根堆),节点向上的通路是相对排序的,但横向相邻的元素却没有必然的大小。 // // 把一个线性数组理解为一个二叉树,那么索引i处元素的直接下级的第一个元素为2i+1。同层元素顺序存储 // 证明:设i所在树的层为a,其在改层第b个节点,则 i = 2^a-1+b, 其直接下级的第一个元素索引 j = 2^(a+1)-1+2b = 2i+1 void shift_down(int* pData, int l, int r) { int j, jold; int a = pData[l]; jold = l; // jold用于存储a归位时的位置 j = 2 * l + 1; // j为l的直接下属的第一个元素 while (j <= r) { // 与下属中较大的比较,找到比后面相邻的元素较大的元素 if(j < r && pData[j] < pData[j+1]) j++; // 找到a所在的层,停止筛选。否则更新a,继续 if (a >= pData[j]) break; // 把较大的元素提前 pData[jold] = pData[j]; jold = j; // 更新a将要归位的位置 j = 2 * j + 1; } pData[jold] = a; // 将a归位 } // 堆排序 void HeapSort(int* pData, int nCount) { // 创建堆 for(int i = nCount/2-1; i >= 0; i--) // 堆创建时,i作为shift_down的左范围从n/2-1 to 0 shift_down(pData, i, nCount-1); // 堆排序 for(int i = nCount-1; i > 0; i--) { // 堆选择时,shift_down的右范围从n-2 to 0 std::swap(pData[0], pData[i]); // 在数组尾部清出一个空间,使堆顶弹出并存入其中 shift_down(pData, 0, i-1); // 重建堆 } }
堆排序的平均时间复杂度为O(nlogn),空间复杂度为Θ(1)。
8、SHELL排序
// 希尔排序,其中m数组中已有m个元素已排序 // 思路:直接插入法的变形。选取一个逐渐递减的增量序列,按此增量对数组进行分组按直接插入法排序,然后减小增量再分组排序 void ShellSort(int* pData, int nCount, int m) { int i, j, inc, n = nCount; int v; if (m > 0) n = min(m, n); inc = 1; // 确定初始增量 do { inc *= 3; inc++; } while (inc <= n); do { // 部分排序的循环 inc /= 3; // 减小增量 for (i = inc; i < n; i++) { // 直接插入法的外循环 v = pData[i]; j = i; while (pData[j - inc] > v) { // 直接插入法的内循环 pData[j] = pData[j - inc]; j -= inc; if (j < inc) break; } pData[j] = v; } } while (inc > 1); }
三、其他排序
9、双向冒泡: 又叫鸡尾酒排序
通常的冒泡是单向的,而这里是双向的,也就是说还要进行反向的工作。
代码看起来复杂,仔细理一下就明白了,是一个来回震荡的方式。
// 双向冒泡排序 void Bubble2Sort(int* pData, int nCount) { int i; int left = 1; int right =nCount -1; int t; do { //正向的部分 for (i = right; i >= left; i--) { if(pData[i] < pData[i-1]) { std::swap(pData[i], pData[i-1]); t = i; } } left = t+1; //反向的部分 for(i = left; i < right + 1; i++) { if(pData[i] < pData[i-1]) { std::swap(pData[i], pData[i-1]); t = i; } } right = t-1; }while(left <= right); }
双向冒泡排序最糟或是平均所花费的次数都是O(n2),但如果序列在一开始已经大部分排序过的话,会接近O(n)。
10、计数排序
算法的步骤如下:
// 计数排序 void CountSort(int* pData, int nCount) { int i, j; // 计算排序数组的可取值范围 int minNum = pData[0], maxNum = pData[0]; for(i = 1; i < nCount; i++) { if(pData[i] < minNum) minNum = pData[i]; else if(pData[i] > maxNum) maxNum = pData[i]; } // 计数数组 int range = maxNum - minNum + 1; int *pTemp = new int[range]; memset(pTemp, 0, range * sizeof(int)); // 如果元素存在,对应位置计数加1 for(i = 0; i < nCount; ++i) { pTemp[pData[i]-minNum]++; } // 按顺序输出 int k = 0; for(i = minNum; i <= maxNum; ++i) for(j = 0; j < pTemp[i - minNum]; ++j) pData[k++] = i; delete pTemp; }
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。
11、基数排序
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零. 然后, 从最低位开始, 依次进行一次排序.这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列.
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
// 基数排序 // 思路:对数组元素每一位进行排序 void RadixSort(int* pData, int nCount, int maxDigits) { const int base = 10; int arCountSort[10]; int* pTemp = new int[nCount]; int i, j; int t = 1; // LSD for(i = 0; i < maxDigits; ++i) { // 计数排序算子 memset(arCountSort, 0, sizeof(int) * 10); for(j = 0; j < nCount; ++j) { arCountSort[pData[j]/t%base]++; } // 为了定位原元素的位置 for(j = 1; j < 10; ++j) { arCountSort[j] += arCountSort[j-1]; } // 按位排序好的数组写入临时数组里 // 此处要用倒序,因为低位已经排过序,同一组优先进低位排序中靠后的 for(j = nCount-1; j >= 0; --j) { int splitDigit = pData[j]/t%base; pTemp[arCountSort[splitDigit] - 1] = pData[j]; arCountSort[splitDigit]--; } memcpy_s(pData, sizeof(int) * nCount, pTemp, sizeof(int) * nCount); t *= base; } }
MSD算法代码:
// MSD法,基数排序+计数排序 void RadixSort(int* pData, int index, int nCount, int maxDigits) { if(nCount <= 1 || maxDigits <= 0) return; const int base = 10; int arCountSort[base]; // 各位元素计数数组 int arCountSortTemp[base]; int* pTemp = new int[nCount]; int i; int t = (int)pow((float)base, maxDigits-1); // 计数排序算子 memset(arCountSort, 0, sizeof(int) * base); for(i = index; i < index + nCount; ++i) { arCountSort[pData[i]/t%base]++; } memcpy_s(arCountSortTemp, sizeof(int)*base, arCountSort, sizeof(int)*base); // 为了定位原元素的位置,此处每个arCountSort数组存放的是当前位前所有值的个数 for(i = 1; i < base; ++i) { arCountSort[i] += arCountSort[i-1]; } for(i = 0; i < base; ++i) { arCountSort[i] -= arCountSortTemp[i]; } // 正序输出 for(i = index; i < index + nCount; ++i) { int splitDigit = pData[i]/t%base; // splitDigit也为当前值在arCountSort表示的桶内的索引 pTemp[arCountSort[splitDigit]] = pData[i]; arCountSort[splitDigit]++; } memcpy_s(pData + index, sizeof(int) * nCount, pTemp, sizeof(int) * nCount); // 递归 RadixSort(pData, index, arCountSort[0], maxDigits-1); for(i = 1; i < base; ++i) { RadixSort(pData, index + arCountSort[i-1], arCountSort[i] - arCountSort[i-1], maxDigits-1); } delete pTemp; } void RadixSortMSD(int* pData, int nCount, int maxDigits) { RadixSort(pData, 0, nCount, maxDigits); printf_s("基数排序结果:"); for(int i = 0; i < nCount; i++) { printf_s("%d ", pData[i]); } printf_s("/n"); }