正所谓人如其名,冒泡排序正是使用了“冒泡”的方法对元素进行了排序。
它的算法思想就是在每次遍历的时候从头到尾比较相邻的两个元素大小,将较小的元素“冒”到前面来,把最大的元素移向队尾,使得元素变得有序。
我们把已经排好序的区域称为有序区,相对应的便是没排好顺序的无序区了。
那么冒泡排序大致可以分为两类,一类是每次遍历的时候将较小的元素“冒”到前面来,把最小的元素移向队首,使得前面的元素变得有序,即将前排元素逐渐扩展成有序区,并且慢慢缩小后排元素所在的无序区。另一类则是将较大的元素“冒”到后面去,把最大的元素移向队尾,使得后面的元素变得有序,即将后排元素逐渐扩展成有序区,并且慢慢缩小前排元素所在的无序区。这两种在本质上没有任何区别,是排序家族的冒泡双胞胎。下面来看一下冒泡算法的代码示例:
- #include <iostream>
- using namespace std;
- template <class T>
- void BubbleSort(T& nData,int len)
- {
- for(int i = 0;i<len-1;++i){
- for(int j = len - 1;j > i;--j){
- if(nData[j]<nData[j-1]){
- int temp = nData[j];
- nData[j] = nData[j-1];
- nData[j-1] = temp;
- show(nData,len);
- }
- }
- }
- }
- template <class T>
- void show(T arr,int n){
- for(int i =0;i<n-1;i++){
- cout<<arr[i]<<",";
- }
- cout<<arr[n-1]<<endl;
- }
- void main()
- {
- int inputNumber[]={2,7,5,9,1,4,6,3,8};
- int count = 9;
- cout<<"原始数组:"<<endl;
- show(inputNumber,count);
- cout<<"排序过程:"<<endl;
- BubbleSort(inputNumber,count);
- cout<<"排序结果:"<<endl;
- show(inputNumber,count);
- }
运行结果如下图所示:
但是如果是这样单纯的冒泡排序在时间上存在很大的浪费,比如现在序列顺序是12345,那么冒泡排序还是老老实实的挨个将它们依次比较并进行排序。所以对于前面说到的冒泡算法还需要进行优化处理:如果某一轮遍历比较中没发生元素交换,则表示整个序列已经有序,排序提前结束。
下面来看一下升级版本也就是能够提前终止的冒泡算法源码:- #include <iostream>
- using namespace std;
- template <class T>
- void BubbleSort(T& nData,int len)
- {
- bool isOk = false;
- for(int i = 0;i<len-1&&!isOk;++i){
- isOk = true;
- for(int j = len - 1;j > i;--j){
- if(nData[j]<nData[j-1]){
- int temp = nData[j];
- nData[j] = nData[j-1];
- nData[j-1] = temp;
- isOk = false;
- show(nData,len);
- }
- }
- }
- }
- template <class T>
- void show(T arr,int n){
- for(int i =0;i<n-1;i++){
- cout<<arr[i]<<",";
- }
- cout<<arr[n-1]<<endl;
- }
- void main()
- {
- int inputNumber[]={2,7,5,9,1,4,6,3,8};
- int count = 9;
- cout<<"原始数组:"<<endl;
- show(inputNumber,count);
- cout<<"排序过程:"<<endl;
- BubbleSort(inputNumber,count);
- cout<<"排序结果:"<<endl;
- show(inputNumber,count);
- }
代码的运行结果如下图所示:
下面来分析一下冒泡排序的复杂度。
当最好的情况,也就是要排序的表本身就是有序的,那么我们比较次数,根据最后改进的代码,可以推断出就是n‐1次的比较,没有数据交换,时间复杂度为O(n)。
当最坏的情况,即待排序表是逆序的情况,此时需要比较的次数为:
并作等数量级的记录移动。因此,总的时间复杂度为O(n2)。
再来看一下冒泡排序的稳定性。
排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj, Ai原来在位置前,排序后Ai还是要在Aj位置前。
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
总结一下冒泡排序:
原理:将序列划分为无序和有序区,不断比较无序区的相邻元素,通过交换较大元素至无序区尾完成排序。
要点:设计交换判断条件,提前结束已经排好序的序列循环。2.选择排序:
选择排序可以说是最容易理解的排序算法,它的思想和正常人脑的思路基本一致:先把最小的那个拎出来,逐渐减少排序的规模。在我们给学生按照高矮排序的时候,一般都是先把最矮的学生排在队伍的最前面,然后再从剩下的学生中挑选最矮的排在剩下的学生队伍的最前面。
有人可能会感觉选择排序和冒泡排序很相似,因为都是将最大值放到队尾或最小值放到队首的操作。他们之间的不同点在于,选择排序每扫描一遍数组,只需要一次真正的交换,而冒泡可能需要很多次。但是比较的次数是一样的。
和冒泡排序一样,我们把序列分割成有序区和无序区,每一次的操作都是将无序区中的最小的元素放到无序区的头部,并且将其纳为有序区的成员。所以每次遍历有序区元素个数加一并且无序区元素个数减一。下面来看一下选择排序的源码:
- #include <iostream>
- using namespace std;
- template <class T>
- void SelectionSort(T a[],int n)
- {//对数组a[0:n-1]中的n个元素进行排序
- for(int size=n;size>1;size--)
- {
- int j=Max(a,size);
- Swap(a[j],a[size-1]);
- show(a,n);
- }
- }
- template<class T>
- void Swap(T&a,T&b)
- {
- T temp = a;
- a = b;
- b = temp;
- return;
- }
- template<class T>
- int Max(T a[],int n)
- {//寻找a[0;n-1]中的最大元素
- int pos=0;
- for(int i=1;i<n;i++)
- if(a[pos]<a[i])
- pos=i;
- return pos;
- }
- template <class T>
- void show(T arr,int n){
- for(int i =0;i<n-1;i++){
- cout<<arr[i]<<",";
- }
- cout<<arr[n-1]<<endl;
- }
- void main()
- {
- int inputNumber[]={2,7,5,9,1,4,6,3,8};
- int count = 9;
- cout<<"原始数组:"<<endl;
- show(inputNumber,count);
- cout<<"排序过程:"<<endl;
- SelectionSort(inputNumber,count);
- cout<<"排序结果:"<<endl;
- show(inputNumber,count);
- }
上述选择排序函数的一个缺点是:即使元素已经按序排列,程序仍然继续运行。为了终止不必要的循环,在查找最大元素的期间,可以顺便检查数组是否已按序排列。可以在遍历该数组的过程中添加一个名为sorted的布尔值来判断当前序列是否已经按照顺序排列完毕。下面程序给出了一个按照这种思想实现的选择排序函数。在该函数中,把查找最大元素的循环直接与函数SelectionSort合并在一起,而不是把它作为一个独立的函数:
- #include <iostream>
- using namespace std;
- template <class T>
- void SelectionSort(T a[],int n)
- {//及时终止的选择排序
- bool sorted=false;
- for(int size=n;!sorted && (size>1);size--)
- {
- int pos=0;
- sorted=true;
- //找最大元素
- for(int i=1;i<size;i++)
- if(a[pos]<=a[i])
- pos=i;//如果已经按序排列,那么就不会有else的机会,sorted也就一直为true,最红将终止外部for循环。
- else
- sorted=false;//非未按序排列,表示需要外部for循环
- Swap(a[pos],a[size-1]);
- show(a,n);
- }
- }
- template<class T>
- void Swap(T&a,T&b)
- {
- T temp = a;
- a = b;
- b = temp;
- return;
- }
- template <class T>
- void show(T arr,int n){
- for(int i =0;i<n-1;i++){
- cout<<arr[i]<<",";
- }
- cout<<arr[n-1]<<endl;
- }
- void main()
- {
- int inputNumber[]={2,7,5,9,1,4,6,3,8};
- int count = 9;
- cout<<"原始数组:"<<endl;
- show(inputNumber,count);
- cout<<"排序过程:"<<endl;
- SelectionSort(inputNumber,count);
- cout<<"排序结果:"<<endl;
- show(inputNumber,count);
- }
排序结果截图:
下面来分析一下选择排序的复杂度:
从简单选择排序的过程来看,它最大的特点就是交换移动数据次数相当少,这样也就节约了相应的时间。分析它的时间复杂度发现,无论最好最差的情况,其比较次数都是一样的多,第i趟排序需要进行n-i次关键字的比较,此时需要比较的次数为:
而对于交换次数而言,当最好的时候,交换为0次,最差的时候,也就初始降序时,交换次数为n-1次,基于最终的排序时间是比较与交换的次数总和,因此,总的时间复杂度依然为O(n2)。
应该说,尽管与冒泡排序同为O(n2),但简单选择排序的性能上还是要略优于冒泡排序。
下面来看一下选择排序的稳定性:
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
总结一下选择排序:
原理:将序列划分为无序和有序区,寻找无序区中的最小值和无序区的首元素交换,有序区扩大一个,循环最终完成全部排序。要点:设计交换判断条件,提前结束已经排好序的序列循环。
3.插入排序:
插入排序和前面两种排序的原理基本相同,都是使用扩大有序区缩小无序区的方法实现排序。但是插入排序是按照顺序遍历无序区的元素,并且将它们挨个插入到有序区中。借用明哥博客里一个非常恰当的比喻,就好比是打扑克时的插扑克的操作一样。
插入排序的源码如下:- #include <iostream>
- using namespace std;
- template <class T>
- void Insert(T a[],int n,const T &x)
- {//向有序数组a[0:n-1]中插入元素x
- int i;
- for(i=n-1;i>=0 && x<a[i];i--)
- a[i+1]=a[i];
- a[i+1]=x;
- }
- template <class T>
- void InsertionSort(T a[],int n)
- {//对a[0:n-1]进行排序
- for(int i=1;i<n;i++)
- {
- T t=a[i];
- Insert(a,i,t);
- show(a,n);
- }
- }
- template <class T>
- void show(T arr,int n){
- for(int i =0;i<n-1;i++){
- cout<<arr[i]<<",";
- }
- cout<<arr[n-1]<<endl;
- }
- void main()
- {
- int inputNumber[]={2,7,5,9,1,4,6,3,8};
- int count = 9;
- cout<<"原始数组:"<<endl;
- show(inputNumber,count);
- cout<<"排序过程:"<<endl;
- InsertionSort(inputNumber,count);
- cout<<"排序结果:"<<endl;
- show(inputNumber,count);
- }
插入排序的截图如下:
插入排序的复杂度分析:
当最好的情况,也就是要排序的表本身就是有序的,没有移动的记录,时间复杂度为O(n)。
当最坏的情况,即待排序表是逆序的情况,比如{6,5,4,3,2},此时需要比较的次数为:
而记录的移动次数也达到最大值:
如果排序记录是随机的,那么根据概率相同的原则,平均比较和移动次约为四分之一乘以N的平方次。
因此,我们得出直接插入排序法的时间复杂度为O(n2)。从这里也看出,同样的O(n2)时间复杂度,直接插入排序法比冒泡和简单选择排序的性能要好一些。
下面来看一下插入排序的稳定性:
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
原理:将数组分为无序区和有序区两个部分,然后不断的将无序区的第一个元素按照大小的顺序插入到有序区中,每次遍历操作无序区中的元素个数减一并且有序区元素加一。
要点:设立标志,作为临时存储和判断数组边界之用。
4.堆排序
在介绍堆排序之前,还得说一说堆的概念。简单来说,最大堆就是一个从上而下的每一层都满足从大到小顺序的完全二叉树。
那么最简单的堆排序就是每次取出堆顶元素,并且保持剩下的元素依旧构成一个最大堆,就可以实现堆排序了。具体的相关操作参考连接的博文内容。
所以堆排序的排序方法非常简单,因为重点在堆结构的操作上。源码如下:
- //优先队列:堆MaxHeap的定义与使用
- #include <iostream>
- using namespace std;
- void OutOfBounds(){
- cout<<"Out Of Bounds!"<<endl;
- }
- void BadInput(){
- cout<<"Bad Input!"<<endl;
- }
- void NoMem(){
- cout<<"No Memory!"<<endl;
- }
- template<class T>
- class MaxHeap{
- public:
- MaxHeap(int MaxHeapSize = 10);
- int Size()const{return CurrentSize;}
- T Max(){
- if (CurrentSize == 0)
- throw OutOfBounds();
- return heap[1];
- }
- MaxHeap<T>& Insert(const T&x);
- MaxHeap<T>& DeleteMax(T&x);
- void Initialize(T a[],int size,int ArraySize);
- void Output(ostream& out)const;
- int CurrentSize;
- int MaxSize;
- T *heap;//元素数组
- };
- //输出链表
- template<class T>
- void MaxHeap<T>::Output(ostream& out)const{
- for (int i= 0;i<CurrentSize;i++)
- {
- cout<<heap[i+1]<<" ";
- }
- cout<<endl;
- }
- //重载操作符
- template<class T>
- ostream& operator<<(ostream& out,const MaxHeap<T>&x){
- x.Output(out);
- return out;
- }
- template<class T>
- MaxHeap<T>::MaxHeap(int MaxHeapSize /* = 10 */){
- MaxSize = MaxHeapSize;
- heap = new T[MaxSize+1];
- CurrentSize = 0;
- }
- //将x插入到最大堆中
- template<class T>
- MaxHeap<T>& MaxHeap<T>::Insert(const T&x){
- if(CurrentSize==MaxSize)
- throw NoMem();
- //为x寻找插入位置
- //i从新的叶结点开始,并沿着树慢慢上升
- int i = ++CurrentSize;
- while(i!=1&&x>heap[i/2]){
- //不能把x放到heap[i]
- heap[i] = heap[i/2];//将元素下移
- i/=2;
- }
- heap[i] = x;
- return *this;
- }
- //将最大的元素放到x并从堆中删除
- template<class T>
- MaxHeap<T>& MaxHeap<T>::DeleteMax(T&x){
- //检查堆是否为空
- if(CurrentSize==0)
- throw OutOfBounds();
- x = heap[1]; //取出最大元素并放入x中
- T y = heap[CurrentSize]; //y为最后一个元素
- CurrentSize--;
- //从根开始为y寻找合适的位置
- int i = 1; //堆的当前节点
- int ci = 2; //i的孩子
- while(ci<=CurrentSize){
- //heap[ci]应该是较大的孩子
- if(ci<CurrentSize&&heap[ci]<heap[ci+1])
- ci++;
- //能否把y放入heap[i]
- if(y>=heap[ci])
- break;
- heap[i]=heap[ci];
- i = ci;
- ci = 2*ci;
- }
- heap[i]=y;
- return *this;
- }
- //把最大堆初始化为数组a
- template<class T>
- void MaxHeap<T>::Initialize(T a[],int size,int ArraySize){
- delete []heap;
- heap = a;
- CurrentSize = size;
- MaxSize = ArraySize;//数组空间大小
- //产生一个最大堆
- for (int i = CurrentSize/2;i>=1;i--){
- T y = heap[i]; //子树的根
- //寻找放置y的位置
- int c = 2*i; //c的父节点是y的目标位置
- while(c<=CurrentSize){
- //heap[c]应该是较大的同胞节点
- if(c<CurrentSize&&heap[c]<heap[c+1])
- c++;
- //能否把y放入heap[c/2]
- if(y>=heap[c]) //能把y放入heap[c/2]
- break;
- //不能把y放入heap[c/2]
- heap[c/2]=heap[c]; //将孩子上移
- c=2*c; //下移一层
- }
- heap[c/2] = y;
- }
- }
- template<class T>
- void HeapSort(T a[],int n)
- {
- MaxHeap<T>H;
- H.Initialize(a,n-1,20);
- T x;
- for (int i=n-1;i>=1;i--)
- {
- show(a,10);
- H.DeleteMax(x);
- a[i]=x;
- }
- }
- template <class T>
- void show(T arr,int n){
- for(int i =1;i<n-1;i++){
- cout<<arr[i]<<",";
- }
- cout<<arr[n-1]<<endl;
- }
- int main(){
- MaxHeap<int>myHeap;
- const int number = 10;
- int myArray[number+1] = {-1,2,7,5,9,1,4,6,3,8};
- cout<<"原始数组:"<<endl;
- show(myArray,number);
- cout<<"排序过程:"<<endl;
- // myHeap.Initialize(myArray,number,20);
- HeapSort(myArray,number);
- cout<<"排序结果:"<<endl;
- show(myArray,number);
- return 0;
- }
代码执行的过程图如下:
下面来分析一下堆排序的复杂度。
它的运行时间主要是消耗在初始构建堆和在重建堆时的反复筛选上。
在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端结点开始构建,
将它与其孩子进行比较和若有必要的互换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)。
在正式排序时,第i次取堆顶记录重建堆需要用O(log i)的时间。完全二叉树的某个结点到根结点的距离为log(i+1),并且需要取n-1次堆顶记录,因此,重建堆的时间复杂度为O(nlogn)。
所以总体来说,堆排序的时间复杂度为O(nlogn)。
由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlogn)。这在性能上显然要远远好过于冒泡、简单选择、直接插入的O(n2)的时间复杂度了。
空间复杂度上,它只有一个用来交换的暂存单元,也非常的不错。不过由于记录的比较与交换是跳跃式进行,因此堆排序也是一种不稳定的排序方法。
另外,由于初始构建堆所需的比较次数较多,因此,它并不适合待排序序列个数较少的情况。
下面来分析一下堆排序的稳定性:
我们知道堆的结构是节点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个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
下面来总结一下堆排序的要点:
原理:利用大根堆或小根堆思想,首先建立堆,然后将堆首与堆尾交换,堆尾之后为有序区。
要点:建堆、交换、调整堆。