常见排序算法总结 C++

来源:互联网 发布:蚂蚁影院电影网站源码 编辑:程序博客网 时间:2024/06/01 04:00

1排序简介

1.1 各排序算法的思想及其性质

以下排序算法的排序结果若无特殊说明均为升序,主要讲述算法的简单原理,时间复杂度,空间复杂度和稳定性。其中:时间复杂度简单来说就是算法中基本操作重复执行的次数;空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间;稳定性通俗地讲就是能保证排序中相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。

注:在每种算法的后面会附上算法的C++实现,每种实现都是以int型为例,可以根据自己的需要修改类型(只需修改vector内的类型即可,其他的不用修改),有兴趣的可以实现模板,这样就可以直接调用。

 

 

2 算法及实现

2.1 冒泡排序

依次比较相邻的数据,将小数据放在前,大数据放在后;即第一趟先比较第1个和第2个数,大数在后,小数在前,再比较第2个数与第3个数,大数在后,小数在前,以此类推则将最大的数"滚动"到最后一个位置;第二趟则将次大的数滚动到倒数第二个位置......第n-1(n为无序数据的个数)趟即能完成排序。过程分为有序区和无序区,初始时有序区为空,所有元素都在无序区,经过第一趟后就能找出最大的元素,然后重复便可。

更好的做法是设置一个标志,如果第一次比较完没有交换即说明已经有序,不应该进行下一次遍历还有已经遍历出部分有序的序列后,那部分也不用进行遍历,即发生交换的地方之后的地方不用遍历。

时间复杂度最坏的情况是反序序列,要比较n(n-1)/2次,时间复杂度为O(n^2 ),最好的情况是正序,只进行(n-1)次比较,不需要移动,时间复杂度为O(n)(这是在设置标志后的效率,如果没有则最好还是O(n^2 )),而平均的时间复杂度为O(n^2 )。

空间复杂度为O(1)。

冒泡排序是稳定的排序算法,元素较少时效率比较高。

 

【代码】


void bubbleSort1(vector<int>& v) {int vl(v.size());bool flag(1);for (int k1(0); k1 < vl - 1 && flag; ++k1) {flag = false;for (int k2(vl - 1); k1 < k2; --k2) {if (v[k2] < v[k2 - 1]) swap(v[k2], v[k2 - 1]), flag = true;}}}void bubbleSort2(vector<int>& v) {int vl(v.size());for (int k1(0); k1 < vl - 1; ++k1) {for (int k2(0); k2 < vl - 1 - k1; ++k2) {if (v[k2 + 1] < v[k2]) swap(v[k2 + 1], v[k2]);}}}


注:模板可以这样写(下面的可以类似实现模板,就不再一一列出)

template<typename T> void bubbleSort1(vector<T>& v) {int vl(v.size());bool flag(1);for (int k1(0); k1 < vl - 1 && flag; ++k1) {flag = false;for (int k2(vl - 1); k1 < k2; --k2) {if (v[k2] < v[k2 - 1]) swap(v[k2], v[k2 - 1]), flag = true;}}}template<typename T> void bubbleSort2(vector<T>& v) {int vl(v.size());for (int k1(0); k1 < vl - 1; ++k1) {for (int k2(0); k2 < vl - 1 - k1; ++k2) {if (v[k2 + 1] < v[k2]) swap(v[k2 + 1], v[k2]);}}}


 

2.2 选择排序(普通排序)

以升序为例,比如在一个长度为N的无序数组中,在第一趟遍历N个数据,找出其中最小的数值与第一个元素交换,第二趟遍历剩下的N-1个数据,找出其中最小的数值与第二个元素交换......第N-1趟遍历剩下的2个数据,找出其中最小的数值与第N-1个元素交换,至此选择排序完成。

选择排序的最大时间代价,最小时间代价和平均时间代价均为θ(n²)。空间复杂度为O(1)。选择排序不依赖于原始数组的输入顺序。

注:冒泡法和选择排序很像,两者区别在于:冒泡排序是每一次都可能要交换,而选择排序是在比较时记下最小数的位置最后来交换,所以他们的交换过程是不一样的,但查找的过程是一样的。因此选择排序的效率比冒泡法只高不低。

 

【代码】


void selectSort(vector<int> &v) {      int vl(v.size());      for (int k1(0), km(0); k1 < vl - 1; ++k1, km = k1) {                for (int k2(k1 + 1); k2 < vl; ++k2) if(v[k2] < v[km]) km = k2;                swap(v[km], v[k1]);      }}


 

2.3 插入排序

像扑克摸牌一样,插入即表示将一个新的数据插入到一个有序数组中,并继续保持有序。例如有一个长度为N的无序数组,进行N-1次的插入即能完成排序;第一次,数组第1个数认为是有序的数组,将数组第二个元素插入仅有1个有序的数组中;第二次,数组前两个元素组成有序的数组,将数组第三个元素插入由两个元素构成的有序数组中......第N-1次,数组前N-1个元素组成有序的数组,将数组的第N个元素插入由N-1个元素构成的有序数组中,则完成了整个插入排序。

插入排序的时间复杂度最好的情况是已经是正序的序列,只需比较(n-1)次,时间复杂度为O(n),最坏的情况是倒序的序列,要比较n(n-1)/2次,时间复杂度为O(n^2 ) ,平均的话要比较时间复杂度为O(n^2 )。

空间复杂度为O(1)。

插入排序是一种稳定的排序方法,排序元素比较少的时候很好,大量元素便会效率低下。

 

【代码】


void insertSort(vector<int> &v) {    int vl(v.size());    for (int k1(1), k2, tem; k1 < vl; ++k1) {          for (tem = v[k1], k2 = k1; 0 < k2 && tem < v[k2 - 1]; --k2) v[k2] = v[k2 - 1];          v[k2] = tem;    }}


 

 

2.4 希尔(shell)排序

希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些(增量为2的shell排序的时间代价可以达到θ(n的3/2次方),有的增量可以达到θ(n的7/6次方),很接近θ(n))。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。空间复杂度为O(1)。

 

【代码】


void shellSort(vector<int> &v) {    int vl(v.size());    for (int kd(vl / 2); kd; kd >>= 1) {          for (int k1(0); k1 < kd; ++k1) {                    for (int k2(k1 + kd); k2 < vl; k2 += kd) {                               for (int k3(k2); k1 < k3; k3 -= kd) if (v[k3] < v[k3 - kd]) swap(v[k3], v[k3 - kd]);                    }          }    }}


 

 

2.5 归并排序

归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。

归并排序的时间复杂度都是O(nlogn),并且归并排序不依赖于原始数组的有序程度。适用于元素较多的时候排序。空间复杂度为O(n)。

 

【代码(递归版本)】


void merge(vector<int> &v, int low, int mid, int high) { // [low, mid], [mid+1, high];vector<int> v1(v.begin() + low, v.begin() + mid + 1), v2(v.begin() + mid + 1, v.begin() + high + 1);v1.push_back(INT_MAX); v2.push_back(INT_MAX);for (int k1(0), k2(0); low <= high;) v1[k1] < v2[k2] ? v[low++] = v1[k1++] : v[low++] = v2[k2++];}void mergeSort(vector<int> &v, int low, int high) {if (low < high) {int mid((low + high) / 2);mergeSort(v, low, mid);mergeSort(v, mid + 1, high);merge(v, low, mid, high);}}// 为了调用方便,写了这个重载的版本,也可以直接调用 mergeSort(v, 0, v.size() - 1);void mergeSort(vector<int> &v) { mergeSort(v, 0, v.size() - 1); }




 

 

【代码(迭代版本)】


void merge(vector<int> &v, int low, int mid, int high) { // [low, mid], [mid+1, high];      vector<int> v1(v.begin() + low, v.begin() + mid + 1), v2(v.begin() + mid + 1, v.begin() + high + 1);    int vl1(mid - low + 1), vl2(high - mid), k1(0), k2(0);    while (k1 < vl1 && k2 < vl2) v1[k1] < v2[k2] ? v[low++] = v1[k1++] : v[low++] = v2[k2++];    while (k1 < vl1) v[low++] = v1[k1++];    while (k2 < vl2) v[low++] = v2[k2++];} void mergeSort(vector<int> &v) {    int vl(v.size());    for (int k1, kd(1); kd < vl; kd *= 2) {          for (k1 = 0; k1 < vl + 1 - 2 * kd; k1 += 2 * kd) {                    merge(v, k1, k1 + kd - 1, k1 + 2 * kd - 1);          }          if (k1 + kd < vl) merge(v, k1, k1 + kd - 1, vl - 1);    }}


 

 

2.6 快速排序

快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,该方法的基本思想是:先从数列末尾取出一个数作为基准元;将小于它的数全放到它的左边,不小于这个数的全放到它的右边;再对左右区间重复第二步,直到各区间只有一个数。

快速排序时间复杂度的最好情况和平均情况一样为O(nlog2 n),最坏情况下为O(n^2 )。空间复杂度为O(1)。快速排序是一种不稳定的排序方式,其性能依赖于原始数组的有序程度,更进一步分析,就是依赖与轴值元素的选择。快排的比较次数远多于移动次数,所以主要考虑比较次数。快速排序适用于元素多的情况。

 

【代码】


int partition(vector<int> &v, int left, int right) {    int kt(left);    for (int k1(left); k1 < right; ++k1) {          if (v[k1] < v[right]) swap(v[kt++], v[k1]);    }    swap(v[kt], v[right]);    return kt;} void quickSort(vector<int> &v, int left, int right) {    if (right <= left) return;    int pivot = partition(v, left, right);    quickSort(v, left, pivot - 1);    quickSort(v, pivot + 1, right);} void quickSort(vector<int> &v) { quickSort(v, 0, v.size() - 1); }


 

 

2.7 堆排序

我们知道堆的结构是节点k的孩子为2*k+1和2*(k+1)节点,最大堆要求父节点大于等于其2个子节点,所以堆顶是最大的元素。堆排序操作过程如下:初始化堆:将A[0..n-1]构造为堆;将当前无序区的堆顶元素A[0]同该区间的最后一个记录交换,然后将新的无序区调整为新的堆。因此对于堆排序,最重要的两个操作就是构造初始堆和调整堆,其实构造初始堆事实上也是调整堆的过程,只不过构造初始堆是对所有的非叶节点都进行调整。

堆排序的时间复杂度最好到最坏都是O(nlogn),较多元素的时候效率比较高,不依赖于原始数组的有序程度。空间复杂度为O(1)。堆排序不是稳定的排序算法。

 

【代码】


bool maxHeapIfY(vector<int>& heap, int heap_size, int index) {    int left((index << 1) + 1), right((index + 1) << 1), maxindex(left < heap_size && heap[index] < heap[left] ? left : index);    maxindex = right < heap_size && heap[maxindex] < heap[right] ? right : maxindex;    if (index != maxindex) {          swap(heap[maxindex], heap[index]);          maxHeapIfY(heap,heap_size,maxindex);    }    return true;} void buildMaxHeap(vector<int>& heap, int heap_size) {    for (int k1(heap_size / 2 - 1); k1 >= 0; --k1) maxHeapIfY(heap, heap_size, k1);} void heapSort(vector<int>& heapv) {    int vl(heapv.size());    buildMaxHeap(heapv, vl);    for (int k1(vl - 1); 0 < k1; --k1) swap(heapv[k1], heapv[0]), maxHeapIfY(heapv,k1,0);}


 

 

2.8 线性排序

线性排序包括计数排序,基数排序和桶排序。详情请见博客线性排序算法(计数排序和桶排序) C++