以下是个人总结的排序算法,它分为以下几个类:
交换排序:冒泡排序(BubbleSort)和快速排序(QuickSort)。
插入排序:直接插入排序和希尔排序(ShellSort)。
选择排序:选择排序(SelectSort)和堆排序(HeapSort)。
归并排序:分治的思想。
基数排序:稳定快速的排序。
计数排序:对于待排序元素分布较集中的序列有优势。
(一)交换排序:
void BubbleSort(int* arry, int size) { for (int i = 0; i < size - 1; i++) { for (int j = 0; j < size - i - 1; j++) { if (arry[j] > arry[j + 1]) { swap(arry[j], arry[j + 1]); } } } } //冒泡排序第一次优化(设置一个标志) void BubbleSort(int* arry, int size) { bool swaped = false; for (int i = 0; i < size; i++) { swaped = false; for (int j = 0; j < size - i - 1; j++) { if (arry[j] > arry[j + 1]) { swap(arry[j], arry[j + 1]); swaped = true; } } if (!swaped) { break; } } } //冒泡排序第二次优化(去除已经有序的区间) void BubbleSort(int* arry, int size) { int lastswappos = size-1; int lastswappos_temp = size-1; for (int i = 0; i < size; i++) { lastswappos = lastswappos_temp; for (int j = 0; j < lastswappos; j++)//每次冒泡到最后一次交换的位置 { if (arry[j] > arry[j + 1]) { swap(arry[j], arry[j + 1]); lastswappos_temp = j; } } if (lastswappos==lastswappos_temp) { break; } } }
冒泡排序是最简单的交换排序,在这里我们给出了2种优化方式。第一种是设置一个标志位标志在一趟排序中是否有过排序,如果没有则就排序完成,这种优化方式对于在一个数组的后半部分是有序的情况下提高了效率。第二种方式也相当于设置一个标志位,只是这个标志位是标志最后一次交换的数的下标。这种方法是有效的去除了已经有序的区间。
不管怎么优化,对于一般的数组冒泡排序的时间复杂度为O(N^2)。
快速排序:
//挖坑法 void QuickSort(vector<int> &arry, int left, int right) { if (left >= right) return; int lflag = left; int rflag = right - 1; int tmp = arry[left]; while (lflag < rflag) { while ((lflag < rflag) && arry[rflag] >= tmp) { rflag--; } arry[lflag] = arry[rflag]; while ((lflag < rflag) && arry[lflag] <= tmp) { lflag++; } arry[rflag] = arry[lflag]; } swap(arry[lflag], tmp); QuickSort(arry, left, rflag - 1);//左子区间 QuickSort(arry, rflag + 1, right);//右子区间 } //prev/cur法 [left,right] void QuickSort(vector<int> &arry, int left, int right) { //递归结束条件 if (left >= right) return; int cur = left; int prev = cur - 1; int key = arry[right]; while (cur != right) { if (arry[cur] < key && cur != (++prev)) { swap(arry[cur], arry[prev]); } cur++; } swap(arry[++prev],arry[right]); QuickSort(arry, left, prev - 1); QuickSort(arry, prev + 1, right); } //三数取中法 //三数取中法(选取left,right,mid中不大不小的那个值作为key,和arry[right]交换) int FindMidValue(vector<int> &arry, int left, int right)//[p,q] { int mid= left-( left - right ) / 2; if (arry[left] > arry[mid]) { if (arry[mid] > arry[right]) { swap(arry[right], arry[mid]); } else if (arry[right] > arry[left]) { swap(arry[right], arry[left]); } } else if (arry[left] > arry[right]) { swap(arry[right], arry[left]); } return arry[right]; } void QuickSort(vector<int> &arry, int left, int right) { if (left >= right) return; int lflag = left; int rflag = right; int key = FindMidValue(arry, lflag, rflag); while (lflag < rflag) { while (lflag < rflag && arry[lflag] <= key) { lflag++; } if (lflag < rflag) { arry[rflag] = arry[lflag]; rflag--; } while (lflag < rflag && arry[rflag] >= key) { rflag--; } if (lflag < rflag) { arry[lflag] = arry[rflag]; lflag++; } } arry[lflag] = key; QuickSort(arry, left, lflag - 1); QuickSort(arry, lflag + 1, right); }
快速排序是利用了划分子问题的思想。每次选取区间的第一个数据作为中间值,将大于中间值的数据放在右边,否则放在左边。再将中间值的左右边看成一个子区间,递归的划分子区间,直到区间的左边下标大于或者等于右边下标结束。
prev/cur法是利用两个指针,prev和cur,cur一直找小于key值得下标和prev下标的值交换。
而三数取中法是对于快速排序的优化,有时候常常会出现很尴尬的情况:我们选取最左或最右的值为key值,偏偏这个值是整个待排序列的最小或最大的值,这种情况下快速排序就是效率最慢的情况,变成了慢速排序。这时候三数取中法就帮助我们避免这种情况。
快速排序是效率比大部分排序算法的速度要快。快排一般情况下是最快的排序,而且具有稳定性。但是快速排序是一个递归思想,显然,对于内存有限的机器来说它不是一个好的选择。对于很长很长的数组来说也不是一个好的选择,递归的额层数越多,效率越低。
(二)选择排序:
一般的选择排序
void SelectSort(int* arry, int size) { assert(arry); int flag = 0; while (flag != size - 1) { int min = flag; for (int i = flag + 1; i < size; i++) { if (arry[i] < arry[min]) { min = i; } } swap(arry[min], arry[flag]); flag++; } }
一般的选择排序算法的时间复杂度为O(N^2)。
堆排序
void AdjustDown(int* a, size_t size, size_t parent)//下调函数 { size_t child = parent * 2 + 1; while (child < size) { if (child + 1 < size&&a[child] < a[child + 1]) ++child; if (a[parent] < a[child]) { swap(a[parent], a[child]); parent = child; child = parent * 2 + 1; } else break; } } void HeapSort(int* a, size_t size) { //建堆 for (int i = (size - 2) / 2; i >= 0; --i) //i不能定义为size_t除法会溢出,翻转成一个很大的数 { AdjustDown(a, size, i); } for (size_t i = 0; i < size; ++i) { swap(a[0], a[size - 1 - i]); AdjustDown(a, size - i - 1, 0); } }
堆排序是利用大顶堆的性质,可以快速的找出最大值。依次找出最大值,排序完成。时间复杂度为
O(N*lgN)。
(三)插入排序
直接插入排序:
void InsertSort(int *a, size_t size) { assert(a); for (size_t i = 1; i < size; ++i) { int tmp = a[i]; size_t j = i; while (j > 0 && tmp < a[j - 1]) { a[j] = a[j - 1]; j--; } a[j] = tmp; } }
在数组前面的有序序列里找到合适的位置将待插入数据插入,时间复杂度为O(N^2)。
希尔排序:
//gap为gap/2 void ShellSort(int *a,int size) { int gap = size / 2; while (1 <= gap) { // 把距离为 gap 的元素编为一个组,扫描所有组 for (int i = gap; i < size; i++) { int j = 0; int temp = a[i]; // 对距离为 gap 的元素组进行排序 for (j = i - gap; j >= 0 && temp < a[j]; j = j - gap) { a[j + gap] = a[j]; } a[j + gap] = temp; } gap = gap / 2; // 减小增量 } } //gap为gap/3+1 void HellSort(vector<int>& a, int size) { int gap = size; while (gap > 1) { gap = gap / 3 + 1; for (size_t i = 0; i <size - gap; ++i) { int end = i; int tmp = a[end + gap]; while (end >= 0 && a[end] > a[end+gap]) { a[end + gap] = a[end]; a[end] = tmp; end -= gap; } } } }
gap是间隔增量。它控制序列向有序发展。
我们分割待排序记录的目的是减少待排序记录的个数,并使整个序列向基本有序发展。因此,我们需要采取跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
希尔排序的时间复杂度是在O(N^1.25)~O(N^1.66)之间。希尔排序在逆序时效果最好,有序时效果最差,还不如直接排序。
(四)归并排序
归并排序是建立在分治思想上的一种排序方法,将大的问题划分成小问题,逐个解决。我们在排序的时候也时常用到这种思想,比如一个待排序序列很大很大的时候,我们需要将这个大序列划分成一个个的小序列,归并排序将是一个非常好的选择。
void MergeCell(int* arry,int* tmp, int left1, int right1, int left2, int right2) { int index = 0; int begin = left1; while (left1 <= right1&&left2 <= right2) { if (arry[left1] < arry[left2]) { tmp[index++] = arry[left1++]; } else { tmp[index++] = arry[left2++]; } } while (left1 > right1&&left2 <= right2) { tmp[index++] = arry[left2++]; } while (left2 > right2&&left1 <= right1) { tmp[index++] = arry[left1++]; } for (int i = 0; i < index; ++i) { arry[begin++] = tmp[i]; } } void Merge_(int* arry, int* tmp, int left, int right) { if (left < right) { int mid = left - (left - right) / 2; Merge_(arry, tmp, left, mid); Merge_(arry, tmp, mid+1, right); MergeCell(arry, tmp, left, mid, mid + 1, right); } } void MergeSort(int *arry, int size) { int left = 0; int right = size - 1; int* tmp = new int[size]; Merge_(arry, tmp, left, right); for (int i = 0; i < size; ++i) { cout << arry[i] << " "; } }
(五)计数排序
计数排序的方法就有点简单粗暴了,计数排序适合那种待排序序列元素分布比较叫集中的序列,如果序列的最大值和最小值相差很大的话,效率实在是低。
void CountSort(int* arry, int size) { //找出最大和最小的值,max-min作为开辟空间的大小 int min = arry[0], max = arry[0]; for (int i = 0; i < size; ++i) { if (arry[i] < min) min = arry[i]; if (arry[i] > max) max = arry[i]; } int* count = new int[max - min + 1]; memset(count, 0, (max - min + 1)*sizeof(int)); //统计数组中出现数字的次数 for (int i = 0; i < size; ++i) { ++count[arry[i] - min]; } int index = 0; //遍历计数数组,只要值不为一,就将数组的下标+min拷贝进原始数组,原始数组已经没用了 for (int i = 0; i < max - min + 1; ++i) { while (count[i]--) { arry[index++] = min + i; } } }
(六)基数排序:
基数排序有LSD和MSD两种方法,LSD是从低位向高位排序。MSD是从高位向地位排序。这里以LSD实现
//LSD法 适合位数少的排序(此程序排不了含有负数的序列) void RadixSort(int* arry, int size) { int time = 1; int radix = 10; //统计最大的位数 for (int i = 0; i < size; ++i) { if (arry[i] > radix) { time++; radix *= 10; } } int* count = new int[10]; int* bucket = new int[size]; radix = 1; //控制排序的次数,从低位排到高位 for (int i = 0; i < time; ++i) { memset(count, 0, 10 * sizeof(int)); memset(bucket, 0, size * sizeof(int)); for (int j = 0; j < size; ++j) { //统计每个桶中有多少个数据 ++count[(arry[j]/radix) % 10]; } for (int k = 1; k < 10; ++k) { count[k] = count[k] + count[k - 1]; } for (int k = 0; k < size; ++k) { int index = count[(arry[k] / radix) % 10 - 1]; while (bucket[index] != 0) { index++; if (index >= size) { index = 0; } } bucket[index] = arry[k]; } for (int k = 0; k < size; ++k) { arry[k] = bucket[k]; } radix *= 10; } }
计数排序和基数排序是非比较排序,他们都有适用的场合,熟练掌握多种排序算法是有必要的。
博主的知识有限,文章难免会有纰漏,请多多指正。
本文出自 “稻草阳光” 博客,转载请与作者联系!