正所谓人如其名,冒泡排序正是使用了“冒泡”的方法对元素进行了排序。
它的算法思想就是在每次遍历的时候从头到尾比较相邻的两个元素大小,将较小的元素“冒”到前面来,把最大的元素移向队尾,使得元素变得有序。
我们把已经排好序的区域称为有序区,相对应的便是没排好顺序的无序区了。
那么冒泡排序大致可以分为两类,一类是每次遍历的时候将较小的元素“冒”到前面来,把最小的元素移向队首,使得前面的元素变得有序,即将前排元素逐渐扩展成有序区,并且慢慢缩小后排元素所在的无序区。另一类则是将较大的元素“冒”到后面去,把最大的元素移向队尾,使得后面的元素变得有序,即将后排元素逐渐扩展成有序区,并且慢慢缩小前排元素所在的无序区。这两种在本质上没有任何区别,是排序家族的冒泡双胞胎。下面来看一下冒泡算法的代码示例:
- #include
- using namespace std;
- template <</span>class T>
- void BubbleSort(T& nData,int len)
- {
- for(int i = 0;i
- for(int j = len - 1;j > i;--j){
- if(nData[j]
- int temp = nData[j];
- nData[j] = nData[j-1];
- nData[j-1] = temp;
- show(nData,len);
- }
- }
- }
- }
- template <</span>class T>
- void show(T arr,int n){
- for(int i =0;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
- using namespace std;
- template <</span>class T>
- void BubbleSort(T& nData,int len)
- {
- bool isOk = false;
- for(int i = 0;i
- isOk = true;
- for(int j = len - 1;j > i;--j){
- if(nData[j]
- int temp = nData[j];
- nData[j] = nData[j-1];
- nData[j-1] = temp;
- isOk = false;
- show(nData,len);
- }
- }
- }
- }
- template <</span>class T>
- void show(T arr,int n){
- for(int i =0;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
- using namespace std;
- template <</span>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<</span>class T>
- void Swap(T&a,T&b)
- {
- T temp = a;
- a = b;
- b = temp;
- return;
- }
- template<</span>class T>
- int Max(T a[],int n)
- { //寻找a[0;n-1]中的最大元素
- int pos=0;
- for(int i=1;i
- if(a[pos]
- pos=i;
- return pos;
- }
- template <</span>class T>
- void show(T arr,int n){
- for(int i =0;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
- using namespace std;
- template <</span>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
- 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<</span>class T>
- void Swap(T&a,T&b)
- {
- T temp = a;
- a = b;
- b = temp;
- return;
- }
- template <</span>class T>
- void show(T arr,int n){
- for(int i =0;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
- using namespace std;
- template <</span>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+1]=a[i];
- a[i+1]=x;
- }
- template <</span>class T>
- void InsertionSort(T a[],int n)
- { //对a[0:n-1]进行排序
- for(int i=1;i
- {
- T t=a[i];
- Insert(a,i,t);
- show(a,n);
- }
- }
- template <</span>class T>
- void show(T arr,int n){
- for(int i =0;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个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
原理:将数组分为无序区和有序区两个部分,然后不断的将无序区的第一个元素按照大小的顺序插入到有序区中,每次遍历操作无序区中的元素个数减一并且有序区元素加一。
要点:设立标志,作为临时存储和判断数组边界之用。