8个排序算法
#include <iostream> #include <vector> #include <algorithm> using namespace std; /* 1、插入排序。稳定 (1)原理:逐个处理待排序记录,每个记录与前面已排序子序列进行比较,将其插入子序列中正确位置 (2)复杂度:O(n)-》O(n^2),O(n^2) 最佳:升序。时间复杂度为O(n) 最差:降序。时间复杂度为O(n^2) 平均:对于每个元素,前面有一半元素比它大。时间复杂度为O(n^2) 注意:如果待排序数据已经“基本有序”,使用插入排序可以获得接近O(n)的性能 */ void insertsort(int *data, int n) { if (data == nullptr || n <= 1) return; for (int i = 1; i < n; i++) { for (int j = i; j >= 1 && data[j] < data[j - 1]; j--) //升序 swap(data[j],data[j - 1]); } } /*插入排序优化:设置哨兵保存待排序值,将比其大的都后移一位,最后插入该值*/ void insertsort_optimization(int *data, int n) { if (data == nullptr || n <= 1) return; for (int i = 1; i < n; i++) { int j = i; int tmp = data[j]; for (; j >= 1 && tmp < data[j - 1]; j--) //升序 data[j] = data[j - 1]; data[j] = tmp; } } /* 2、冒泡排序。稳定 (1)原理:从数组的底部比较到顶部,比较相邻元素。 如果下面的元素更小则交换,否则,上面的元素继续往上比较。 这个过程每次使最小元素像个“气泡”似地被推到数组的顶部。 (2)复杂度(bubsort_optimization):On-》On^2 ,On^2 */ void bubsort(int *data, int n) { if (data == nullptr || n <= 1) return; for (int i = 0; i < n - 1; i++) { for (int j = n - 1; j > i; j--) { if (data[j] < data[j - 1])//升序 swap(data[j], data[j - 1]);//#include <algorithm> } } } /* 冒泡排序优化:增加一个变量flag,用于记录一次循环是否发生了交换,如果没发生交换说明已经有序,可以提前结束 避免因已经有序的情况下的无意义循环判断 */ void bubsort_optimization(int *data, int n) { if (data == nullptr || n <= 1) return; bool flag = true; for (int i = 0; i < n - 1 && flag; i++) { flag = false; for (int j = n - 1; j > i; j--) { if (data[j] < data[j - 1])//升序 { swap(data[j], data[j - 1]);//#include <algorithm> flag = true; } } } } /* 3、选择排序。不稳定 (1)原理:每次从未排序的序列中找到最小元素,放到未排序数组的最前面 (2)复杂度:O(n^2) 不管数组是否有序,在从未排序的序列中查找最小元素时,都需要遍历完最小序列,所以最差好平均都是O(n^2) (3)优化:每次内层除了找出一个最小值,同时找出一个最大值(初始为数组结尾)。 将最小值与每次处理的初始位置的元素交换,将最大值与每次处理的末尾位置的元素交换。 这样一次循环可以将数组规模减小2,相比于原有的方案(减小1)会更快 */ void selectsort(int *data, int n) { if (data == nullptr || n <= 1) return; for (int i = 0; i < n - 1; i++) { int lowindex = i; for (int j = i + 1; j < n; j++) { if (data[j] < data[lowindex]) lowindex = j; } swap(data[i], data[lowindex]); } } /* 4、希尔排序。不稳定(插入排序的改进版本) (1)原理:shell排序在不相邻的元素之间比较和交换。 利用了插入排序的最佳时间代价特性,它试图将待排序序列变成基本有序的,然后再用插入排序来完成排序工作 在执行每一次循环时,Shell排序把序列分为互不相连的子序列,并使各个子序列中的元素在整个数组中的间距相同, 每个子序列用插入排序进行排序。每次循环增量是前一次循环的1/2,子序列元素是前一次循环的2倍 最后一轮将是一次“正常的”插入排序(即对包含所有元素的序列进行插入排序) (2)复杂度:O(n^(1.5)) 选择适当的增量序列可使Shell排序比其他排序法更有效,一般来说,增量每次除以2时并没有多大效果,而“增量每次除以3”时效果更好 当选择“增量每次除以3”递减时,Shell排序的 平均 运行时间是O(n^(1.5)) */ void shellsort(int *data, int n) { if (data == nullptr || n <= 1) return; const int incregap = 3;//增量每次除以3 /*遍历所有增量大小。incre是增量。每次增量变上次的1/incregap,子序列元素变上次的incregap倍*/ for (int incre = n / incregap; incre > 0; incre /= incregap) { for (int i = 0; i < incre; i++)//共有incr个子序列需要分别进行插入排序 { /*对子序列进行插入排序,当增量为1时,对所有元素进行最后一次插入排序*/ for (int j = i + incre; j < n; j += incre)//每次插入排序都从子序列的第二个值开始,因为认为第一个值已经有序 { for (int k = j; k > i&&data[k] < data[k - incre]; k -= incre) swap(data[k], data[k - incre]); } } } } /* 5、快速排序。不稳定。快速排序是所有内部排序算法中平均性能最优的排序算法 (1)原理:首先选择一个轴值,小于轴值的元素被放在数组中轴值左侧,大于轴值的元素被放在数组中轴值右侧, 这称为数组的一个分割(partition)。快速排序再对轴值左右子数组分别进行类似的操作 (2)复杂度:O(nlogn)-》O(n^2),O(nlogn) 最佳情况:O(nlogn) 最差情况:每次处理将所有元素划分到轴值一侧,O(n^2) 平均情况:O(nlogn) 快速排序平均情况下运行时间与其最佳情况下的运行时间很接近,而不是接近其最坏情况下的运行时间。 (3)优化: (3.1)最明显的改进之处是轴值的选取,如果轴值选取合适,每次处理可以将元素较均匀的划分到轴值两侧: 三者取中法:三个随机值的中间一个。为了减少随机数生成器产生的延迟,可以选取首中尾三个元素作为随机值 (3.2)当n很小时,快速排序会很慢。因此当子数组小于某个长度(经验值:9)时,此时数组已经基本有序,最后调用一次插入排序完成最后处理 (4)选择轴值有多种方法: 最简单的方法是使用首或尾元素。但是,如果输入的数组是正序或者逆序时,会将所有元素分到轴值的一边。 较好的方法是随机选取轴值 */ int partition(int *data, int start, int end) { //选择尾元素为轴值。较好的方法是随机选取轴值,然后和尾元素交换 int small = start - 1;//分割点 for (int index = start; index < end; index++) { if (data[index] < data[end])//选择尾元素为轴值 { ++small; if (small != index) swap(data[small], data[index]); } } ++small; swap(data[small], data[end]);//将轴值从末尾放到分割点 return small;//返回分割点。其左小于它,右大于它 } void qsort(int *data, int start, int end) { if (data == nullptr || end <= start) return; int index = partition(data, start, end); qsort(data, start, index - 1); qsort(data, index + 1, end); } /* 6、归并排序。稳定。 (1)原理:将一个序列分成两个长度相等的子序列,为每一个子序列排序,然后再将它们合并成一个序列。合并两个子序列的过程称为归并 二路归并:将n个记录分成长度为1的多个子序列,然后两两归并,得到长度为2的子序列,再两两归并,直到得到长度为n的有序序列为止 (2)复杂度:时间:O(nlogn);空间:O(n) logn层递归中,每一层都需要O(n)的时间代价,因此总的时间复杂度是O(nlogn), 该时间复杂度不依赖于待排序数组中数值的相对顺序。因此,是最佳,平均和最差情况下的运行时间 由于需要一个和带排序数组大小相同的辅助数组,所以空间代价为O(n) */ void mergesortcore(int *data, int *temp, int i, int j) { if (i == j) return; int mid = (i + j) / 2; mergesortcore(data, temp, i, mid); mergesortcore(data, temp, mid + 1, j); /*二路归并*/ //data中是两个分别已排序好的子序列,两个归并排序后放入额外空间temp,再用temp更新对应位置的data int index1 = i, index2 = mid + 1, current = i;//二路子序列:index1-mid index2-j(end) while (index1 <= mid && index2 <= j) { if (data[index1] < data[index2]) temp[current++] = data[index1++]; else temp[current++] = data[index2++]; } while (index1 <= mid) temp[current++] = data[index1++]; while (index2 <= j) temp[current++] = data[index2++]; for (current = i; current <= j; current++) data[current] = temp[current]; } void mergesort(int *data, int size) { if (data == nullptr || size <= 1) return; int *temp = new int[size]();//new[] 数组,()初始化为0 mergesortcore(data, temp, 0, size - 1); delete[] temp; } /* 归并排序优化空间(原地归并排序):O1空间,O(nlogn)时间。 不用额外空间,两个有序子序列归并时,如果后一个子序列的值小,则前一个子序列全部后移一位,将后一个子序列的小值放到前一个子序列前 【arking原链接测试数据交换2和9就错误,即mid大于左半部分就错误。但其交换两部分内容的方法很好:分别翻转左右部分,再整体翻转】 */ void mergesort_optimization_core(int *data, int start, int end) { if (start == end) return; int mid = (start + end) / 2; mergesort_optimization_core(data, start, mid); mergesort_optimization_core(data, mid + 1, end); int index1 = start, index2 = mid + 1; while (index1 <= mid && index2 <= end)//二路子序列:index1-mid;index2-end { if (data[index1] > data[index2])//移动第一个子序列:从index1到mid全部后移一位,把原index2处内容放到index1处。更新mid和end { int index2_tmp = data[index2]; for (int t = index2; t > index1; t--) data[t] = data[t - 1]; data[index1] = index2_tmp; ++mid; ++index2; } ++index1;//无论是否移动第一个子序列都要更新index1 } } void mergesort_optimization(int *data, int size) { if (data == nullptr || size <= 1) return; mergesort_optimization_core(data,0,size-1); } /* 完全二叉树:假设一个二叉树有n层,那么如果第1到n-1层的每个节点都达到最大的个数:2, 且第n层的排列是从左往右依次排开的,那么就称其为完全二叉树 堆概念:本身是一个完全二叉树,当二叉树的每个节点都大于等于它的子节点的时候,称为大顶堆; 当二叉树的每个节点都小于它的子节点的时候,称为小顶堆。 (stl的make_heap默认大顶堆,使用函数对象less<int>(),而一般sort排序默认此函数对象意味着升序) 堆性质:将堆的内容从左往右,从上至下层次遍历放入数组, 若一个结点在数组中下标为k,那么它的父结点为(k-1)/2,其子节点为2k+1和2k+2 7、堆排序。不稳定。 (1)原理:首先根据数组构建最大堆,然后每次“删除”堆顶元素(将堆顶元素移至末尾),并调整剩余元素为最大堆。 最后得到的序列就是从小到大排序的序列。 (2)复杂度:Onlogn (根据已有数组构建堆需要O(n)的时间复杂度,每次删除堆顶并调整堆需要O(logn)的时间复杂度,对数组n的排序需要取n-1次堆顶记录 所以总的时间开销为,O(n+nlogn),平均时间复杂度为O(nlogn) (3)应用: (3.1)根据已有元素建堆是很快的,如果希望找到数组中第k大的元素,可以用O(n+klogn)的时间,如果k很小,时间开销接近O(n) (3.2)从10亿个浮点数当中,选出其中最大的10000个:大数据不适宜载入内存,可使用外排,效率低;也可构建topk的最小堆,每次和根比较,替换根后调整堆 */ void heapsortcore(vector<int> &data, int parent, int size) { int leftchild = 2 * parent + 1; if (leftchild < size)//确保有左孩子,否则data[xx]会越界访问 { int rightchild = leftchild + 1; int maxchild = leftchild;//左右孩子中大的那个孩子下标 //存在右孩子,则需取得左右孩子中大的那个节点,与父节点比较,把最大的放到父节点, //如果做了交换,则需继续递归调整后续子树 if (rightchild < size) { if (data[leftchild] < data[rightchild]) maxchild = rightchild; } if (data[parent] < data[maxchild]) { swap(data[parent], data[maxchild]); heapsortcore(data, maxchild, size);//做了交换,继续递归调整后续子树 } } } void heapsort(vector<int> &data) { if (data.size() <= 1) return; //从无序数组开始建堆:从最后一个叶子节点的父节点开始 for (int i = (data.size() - 2) / 2; i >= 0; i--) { heapsortcore(data, i, data.size());//从(size - 2) / 2开始 } //循环将 根节点放到尾部,对除尾部外其余元素重新调整为堆 即可调整数组为有序数组 //(调整则需只从root开始,和其子节点比较并交换,调整后续子树 for (int j = data.size() - 1; j > 0; j--) { swap(data[0], data[j]); heapsortcore(data, 0, j);//实际是做了交换,继续递归调整后续子树(和插入到头部做调整不同,插入会改变树结构,可能需要调整全部,而此交换树结构不变,还可重用之前结果,只需调整对应子树) } } /*针对已经构建好的堆,插入一个元素到尾部,再和parent比较调整为新的堆,只需调整子树 (如果插入到头部,后续节点数组坐标改变,有别的本无须调整的分支可能变成不是最小堆,就需要和开始一样从新建堆,复杂度On>Olog)*/ void insert_heapsort(vector<int> &data, int val) { //从无序数组开始建堆:从最后一个叶子节点的父节点开始 for (int i = (data.size() - 2) / 2; i >= 0; i--) { heapsortcore(data, i, data.size());//从(size - 2) / 2开始 } //针对已经构建好的堆,插入一个元素到尾部,再调整为新的堆,只需调整子树 data.push_back(val); int valindex = data.size() - 1;//从插入元素往前调整 int parent = (valindex - 1) / 2; while (valindex > 0) { if (data[parent] < data[valindex]) { swap(data[parent], data[valindex]); valindex = parent;//为继续调整子树做准备 parent = (valindex - 1) / 2; } else break;//调整到已经比parent小了,由于本就是一个最大堆,只是做调整子树,此处表明所有父节点都比它大,直接break } //堆排序输出 for (int j = data.size() - 1; j > 0; j--) { swap(data[0], data[j]); heapsortcore(data, 0, j);//实际是做了交换,继续递归调整后续子树 } } /* STL的堆排序sort_heap(vi.begin(), vi.end()); 循环使用pop_heap实现 */ void stl_heapsort(vector<int> &data) { if (data.size() <= 1) return; //int end = data.size(); make_heap(data.begin(), data.end());//最大堆 for (int i = 0; i < data.size(); i++) { pop_heap(data.begin(), data.end()-i);//将root元素移到尾部,再对其余元素重新调整堆 //--end; } } /* 8、基数排序。稳定。 (1)原理:桶排序扩展。将整数按位数切割成不同的数字,然后按每个位数分别比较。数位较短的数前面补零 低位是有序的,高位中如果有相同的值,则只需在保持稳定的前提下对高位进行排序,结果自然有序。 (2)复杂度:时间Od(n+k),空间On+k k为基数 空间On+k,k为基数10 时间:需要进行最大数位数d次分配和收集,一趟分配On,一趟收集Ok,总共Od(n+k) */ void radixsortcore(vector<int> &data, int exp) { //data[i]从后往前数的第exp位值(data[i]/exp)%10 为下标的元素个数 vector<int> valuecount(10, 0); for (int i = 0; i < valuecount.size(); i++)//对于较短的数,高位此处计算为0,则后续会排到前面,且相对位置不变 valuecount[(data[i] / exp) % 10]++; //valuecount表示data该出现在排序数组中的下标。valuecount[i]表示i之前的数据出现次数,也即当前数本应该在的位置。 //如果有3个数个位为1,2个数个位为0,则个位为1的3个数下标从3+2-1开始到3+0-1 for (int i = 1; i < 10; i++) valuecount[i] += valuecount[i - 1]; //从数组尾部开始放入排序数组对应位置(根据上面求出的每个值的个数),确保相对位置稳定 vector<int> tmpdata(data.size(), 0); for (int i = data.size() - 1; i >= 0; i--) { tmpdata[valuecount[(data[i] / exp) % 10] - 1] = data[i]; --valuecount[(data[i] / exp) % 10]; } data = tmpdata; } void radixsort(vector<int> &data) { if (data.size() <= 1) return; int max = -1; for (int i = 0; i < data.size(); i++) {//找出最大值 if (data[i] > max) max = data[i]; } for (int exp = 1; max / exp > 0; exp *= 10)//exp从个位到十位到... radixsortcore(data, exp); } int main() { #if 0 //int data[] = { 0,1,5,6,2,9,30,4,7,8 }; //insertsort(data, 10);//插入排序。稳定。 时间On-》On^2,On^2 //insertsort_optimization(data, 10);//插入排序优化。设置哨兵,减少交换 //bubsort(data, 10);//冒泡排序。稳定。 时间On-》On^2 ,On^2 //bubsort_optimization(data, 10);//冒泡排序优化。增加flag,提前结束 //selectsort(data, 10);//选择排序。不稳定。时间O(n^2) //shellsort(data,10);//希尔排序(在插入排序的基础上)。不稳定。时间On^1.5 //qsort(data, 0, 9);//快速排序。不稳定。时间Onlogn-》On^2,Onlogn //mergesort(data, 10);//归并排序。稳定。Onlogn时间,On空间 //mergesort_optimization(data, 10);//归并排序优化。Onlogn时间,O1空间 for (int i = 0; i < sizeof(data) / sizeof(int); i++) cout << data[i] << " "; cout << endl; #endif //#if 0 vector<int> data(10,0); for (int i = 0; i < 10; i++) data[i] = 9-i; cout << "排序前: "; for (vector<int>::iterator it = data.begin(); it != data.end(); it++) cout <<*it << " "; cout << endl; heapsort(data);//堆排序。不稳定。Onlogn //stl_heapsort(data);//stl堆排序 //int val = 10; //insert_heapsort(data,val);//插入元素到已经排序好的堆,重新调整为堆,再排序输出 //radixsort(data);//基数排序,针对非负整数。稳定。时间Od(n+k),空间On+k k为基数 cout << "排序后: "; for (vector<int>::iterator it = data.begin(); it != data.end(); it++) cout << *it << " "; cout << endl; //#endif return 0; }