目录
1.插入排序
1.1直接插入排序
void InsertSort(int* a, int n) { for (int i = 0; i < n - 1; i++) { int end = i; int tmp = a[end + 1]; while (end >= 0) { if (a[end] > tmp) { a[end + 1] = a[end]; end--; } else { break; } } a[end + 1] = tmp; } } 插入排序,是在原数组上进行操作的 从下标0开始,end=i,end是已经排好序的下标 end+1是准备插入的数据下标,赋给tmp, 通过while循坏,让end位置的数据与tmp比较,这里是升序 所以,只要end位置下标数据大于tmp,就让end位置的数据 覆盖到end+1上,让end位置空出来,end再--,直到end下标数据大于等于 tmp,就直接break,因为上次循环end已经--了,此时的end+1就是刚刚空出来的end位置 于是让tmp数据直接覆盖到end+1即可。 注意每次都会让一个位置空出来,其他数据往后挪,直到遇到合适位置插入数据
1.1.1特性:
空间复杂度是:O(1)
时间复杂度:O(N^2)
稳定性:稳定
ps:因为没有开辟新空间,所以空间是O(1);
时间复杂度最坏情况是逆序的数据排升序,那么每次都要把数据遍历到0下标位置才行,所以加起来接近N^2;
稳定性,因为遇到相等值,end跟tmp存的数据一样,tmp数据是覆盖到end+1位置的,相对位置没有变
1.2希尔排序
void ShellSort(int* a, int n) { int gap = n; //gap是一个间隔的大小 //通过gap,把一个数据分成数量不等的多组数据 //希尔排序就是在插入排序的前提下,加入了预排序 //因为插入排序对越接近有序的数据,效率越高 //于是通过预排序,让数据越发接近有序 //gap=1就可以直接当做是插入排序了 while(gap > 1) { gap /= 2;//gap/=3 +1 //gap的变化没有一个好的判断,现在默认每次/2,/3+1也行 // /3+1是要保证最少能1 for (int i = 0; i < n - gap; i++) { //注意,i要小于n-gap,以免越界访问 int end = i; int tmp = a[end + gap]; //这边的写法可以参考插入排序 //只是原来的插入排序,每次都是对+1位置的数据与已排序的数据 //进行比较,找合适位置。 //而这里,当前位置的+gap,才是这组数据应该待插入的数据 while (end >= 0) { if (a[end] > tmp) { a[end + gap] = a[end]; end -= gap; //当tmp小于end位置数据,让end位置数据覆盖到end+gap位置 //因为最初的end+gap数据已经存在tmp了,所以放心的往后挪数据即可 //end要-=gap,因为当前的数据在当前这组数据里的前一个数据是在数组里 //是通过gap大小隔开的。 } else { break; } } a[end + gap] = tmp; //end+gap的位置永远是待覆盖的 } } }
1.2.1特性:
空间复杂度:O(1)
因为没有开辟额外空间。
时间复杂度:O(N^2)
注意,这里是最坏情况,是逆序时,在排接近有序的数据时,反而是非常快的
最好情况是O(1.3)
稳定性:不稳定
因为有可能两个相等的数据被分在了不同的组里,这时就有可能改变相对顺序
2.选择排序
2.1简单选择排序
void SelectSort(int* a, int n) { int begin = 0, end = n - 1; while (begin < end) { int min = begin, max = begin; //设置min和max,记录begin和end范围内的最大值和最小值 //注意,每次必须都重新把max和min的下标改下,以免出现位置错误 for (int i = begin + 1; i <= end; i++) { if (a[i] > a[max])max = i; if (a[i]< a[min])min = i; } //记录begin和end范围内的最大值和最小值 Swap(&a[begin], &a[min]); //把最小值跟begin位置数据交换 //把小的放在前面 if (max == begin)max = min; //防止begin位置就是max值 //否则后面max位置存的是min值,min位置存的是max值 Swap(&a[end], &a[max]); //让max位置数据跟end位置数据交换,把max的放在后面 begin++; end--; //排完两个数,缩减范围 } //循坏条件采用begin<end,当begin=end,说明已经排完了 }
2.1.1特性:
空间复杂度:O(1)
因为没有开辟新空间
时间复杂度:O(N^2)
每次都会遍历begin-end内的数据,一共n次,那么加起来就会接近N^2
稳定性:不稳定
2.2堆排序
void AdjustDown(int* a, int size, int parent) { int child = parent * 2 + 1; while (child < size) { if (child + 1 < size && a[child + 1] > a[child]) { child++; } if (a[child] > a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = parent * 2 + 1; } else { break; } } } //向下调整算法,就是当前面parent的节点数据与孩子比较, //根据升序降序,建小堆还是大堆,把小的或者大的数据往下放 //具体可以看下我的二叉树的文章 void Heapsort(int* a, int size) { assert(a); for (int i = ((size - 1) - 1) / 2; i >= 0; i--) { AdjustDown(a, size, i); } int end = size - 1; while (end > 0) { Swap(&a[0], &a[end]); AdjustDown(a, end, 0); end--; } } //这个我在二叉树的文章里写了,这里简单说下 //通过向下调整算法,把数组最后一个元素当成最后一个叶节点 //然后从最后一个分支节点开始向下调整,这样对于这个分支节点 //所构成的子树来说,已经把子树里数据,按大堆或小堆的规则排列了 //这样不停的往前走,直到从数组下标0的位置开始向下调整,这样 //整个数组都被排列成大堆或小堆了
2.2.1特性:
空间复杂度:O(1)
时间复杂度:O(N*logN)
用满二叉树算,2^h -1=N,h=log2(N+1),而建堆需要O(N)的复杂度,而堆排序,需要把堆顶元素和数组最后一个交换,最终要循环N次,所以,最终是O(N+N*logN),即
O(N*logN)
稳定性:不稳定
对于相等的数据,假如根节点和根节点右孩子相等,那么根节点被置换后,右孩子上位,也被置换,那么两者的相对位置就被改变了
3.交换排序
3.1.冒泡排序
void BubbleSort(int* a, int n) { for (int j = 0; j < n; j++) { //最外面的j是用来控制里面for循环的范围的, //对已经排好的数据,不要循环到 int s1 = 0; //假如有些数据只需要一次循坏就完成有序了 //所以这时候判断有没有进行过一次交换,没有 //交换过,就直接退出循环即可。 for (int i = 1; i < n-j; i++) { if (a[i - 1] > a[i]) { Swap(&a[i - 1], &a[i]); s1 = 1; } //注意,这里的范围,要注意下面的i-1,还是i+1,这决定了最初是1开始 //还是0开始,也决定了i最大是n-1还是n-2 } if (s1 == 0)break; } } 冒泡排序的思想,就是将最大或最小的数,放在最后
3.1.1特性
空间复杂度:O(1)
没有开辟新空间
时间复杂度:O(N^2)
i循坏每次都要遍历接近整个数组的大小,约下来,差不多就是N^2了
稳定性:稳定
对于重复性数据,不会进行交换,就算进行交换,也是第二个重复的数据跟后面的数据交换,不会改变相对位置
3.2快速排序
void QuickSort(int* a, int begin, int end) { if (begin >= end)return; int key = PartSort3(a, begin, end); QuickSort(a, begin, key - 1); QuickSort(a, key + 1, end); } 快排的思想,就是将关键值放在一个合适的位置,让关键值最终的位置左边都小于等于关键值,右边大于关键值 再把关键值左边的部分找个新的关键值,左边小于,右边大于, 关键值部分右边也是,找个新关键值,左边大于,右边小于 不停递归。 这是个通用的函数,具体还是partsort函数部分内容,因为 关于这部分的操作有很多版本,我们一个个讲
3.2.1递归版(1):
int GetMidi(int* a, int begin, int end) { int midi = (begin + end) / 2; if (a[begin] > a[midi]) { if (a[midi] > a[end])return midi; else if (a[begin] > a[end])return end; else return begin; } else { if (a[midi] < a[end])return midi; else if (a[begin] < a[end])return end; else return begin; } } 三数取中,将begin位置和end位置,还有2者中间的位置, 3个位置中找个中间值 可以避免,选的关键值太小,增加时间复杂度
int PartSort1(int* a, int begin, int end) { int left = begin, right = end; int key = begin; //left和right是指向下标 int midi = GetMidi(a, begin, end); Swap(&a[midi], &a[begin]); //三数取中,把中间值跟begin位置交换 while (left < right) { while (left<right && a[right]>=a[key]) { --right; } //当left还小于right且,right还大于等于key位置值,则继续-- //往前找,当小于的时候,停下 while (left < right && a[left] <= a[key]) { ++left; } //同理,从左边开始找大,找到停下 Swap(&a[left], &a[right]); } // Swap(&a[left], &a[key]); //key位置的值跟left位置的值交换, //为什么保证left最终停的位置一定小于等于key呢 //因为我们是先将right--,再left-- //那么只有两种情况, //第一种,right先停下,left走到right位置,这时,left和 //right相等,right是找到小的停下,那么key位置和left交换 //就不会出事 //第二种,right先遇到left,这是left在上次循坏已经跟right交换过了 //这时left位置的数据还是比关键值小的,退出循环后 //left和key交换,还是把比key小的值放入key位置,key值放在left位置上 //这样key就会放在数组中间位置上 key = left; //让left下标赋值给key,left下标此时存的是key值, //再返回下标 return key; } 霍尔版本
3.2.2递归版本(2)
//挖坑法 int PartSort2(int* a, int begin, int end) { int midi = GetMidi(a, begin, end); Swap(&a[midi], &a[begin]); //三数取中 int hole = begin; int key = a[begin]; //挖坑,第一个坑begin位置 //提前把key值存下 while (begin < end) { while (begin < end && a[end] >= key) { end--; } //找小 a[hole] = a[end]; hole = end; //把end位置的值放在hole上,hole位置上的值都是可以被随意覆盖的,因为 //提前被覆盖到别的位置或已经存起来了 while (begin < end && a[begin] <= key) { begin++; } //找大 a[hole] = a[begin]; hole = begin; //值放入hole下标 } a[hole] = key; //最后把key值放在hole上 //返回hole,hole就是关键值下标 return hole; }
3.2.3递归版本3:
//前后指针 int PartSort3(int* a, int begin, int end) { int midi = GetMidi(a, begin, end); Swap(&a[midi], &a[begin]); //三数取中 int prev = begin; int cur = begin + 1; int key = begin; //prev下标位置是用来跟cur交换的 //prev前的数据都是比key小的 while (cur <= end) { if (a[cur] < a[key]) { prev++; Swap(&a[prev], &a[cur]); cur++; } else { cur++; } // 除非找到比key值小的,否则cur都继续往后走 //如果找到了,那就把prev++,让cur位置的值跟prev交换 } Swap(&a[prev], &a[key]); //同理,因为prev在++前,必是小于key的值 //交换即可 key = prev; //同理prev是key值合适的位置,返回即可 return key; }
3.2.4非递归版本:
void QuickSortNotR(int* a, int begin, int end) { ST s1; STInit(&s1); STPush(&s1, end); STPush(&s1, begin); //利用栈的后进先出特性,先把end和begin下标放进去 while (!STEmpty(&s1)) { int left = STTop(&s1); //left就是后进的begin STPop(&s1); int right = STTop(&s1); //right就是先进的end STPop(&s1); int keyi = PartSort3(a, left, right); //利用前面写的内容 if (left < keyi - 1) { STPush(&s1, keyi - 1); STPush(&s1, left); } //左部分放进去,先放keyi-1,再放left if (right>keyi+1) { STPush(&s1, right); STPush(&s1, keyi+1); } //右部分放进去,先放right,再放keyi+1, } STDestory(&s1); }
3.2.5特性:
空间复杂度:O(logN):
递归没有开辟新空间,非递归开辟了,因为递归时空间是可以复用的,但非递归时不行
时间复杂度:O(N*logN)
logN层,每层数据加起来都是N
稳定性:不稳定
因为相同数据相同数据,left和right是不管的,只会++或--
4.归并排序
4.1递归版本
void _MergeSort(int* a, int begin,int end, int *tmp) { if (begin >= end)return; //因为begin=end,说明,此时只有1个数据,已经是有序了,返回即可 //大于,说明,下标超了,也是只有一个数据,不需要 int mid = (begin + end) / 2; _MergeSort(a, begin, mid ,tmp); _MergeSort(a, mid+1, end ,tmp); //从中间位置开始分,把左边部分和右边部分递归 int begin1 = begin, end1 = mid, begin2 = mid + 1, end2 = end; int i = begin; //中间位置,就是两组数据,那么两组数据,各自开始递归,比较大小 while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] <= a[begin2]) { tmp[i++] = a[begin1++]; } else { tmp[i++] = a[begin2++]; } } //把归并后的结果放在tmp数组 while (begin1 <= end1) { tmp[i++] = a[begin1++]; } while (begin2 <= end2) { tmp[i++] = a[begin2++]; } //因为不清楚哪组数据多点,所以这样写即可,把剩下的数据放入tmp memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1)); //再把tmp数组数据覆盖进原数组 } void MergeSort(int* a, int n) { int* tmp = (int*)malloc(sizeof(int)*n); if (tmp == NULL) { perror("malloc fail"); return; } _MergeSort(a, 0, n - 1, tmp); free(tmp); tmp = NULL; } //归并排序,重点在于归并 //每个数据自身都是有序的,2个数据有序,就是两个数据排序,2组数据,每组数据都有2个已经有序的数据 //再把这两组数据归并,就可以让4个数据有序 //最后令整个数组有序即可
4.2非递归版本:
void MergeSortNonR(int* a, int n) { int* tmp = (int*)malloc(sizeof(int) * n); if (tmp == NULL) { perror("malloc fail"); return; } int gap = 1; //这里用gap,在递归里,最后一层,每一个数据都是一组数据 //这时候意味着gap=1,那么倒数第二层就是gap*2, while (gap < n) { for (int i = 0; i < n; i += 2 * gap) { int begin1 = i, end1 = i + gap - 1; int begin2 = i + gap, end2 = i + 2 * gap - 1; int index = i; //i要以2*gap为间隔,以便进入同层的下一组数据 if (begin2 >= n) { break; } if (end2 >= n) { end2 = n - 1; } //因为gap会*2,可能会超下标,下标只有可能是end1,begin2,end2会超 //而begin2超,end1必超,所以begin2超就直接退出循环即可 //begin2超,说明只有1组数据,在最初,就是只有1个数据,那么必然有序,后面再归并就好 //end2超,说明下一组数据没有跟前一组数据数量不一样,那么end2变成n-1即可。 while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] <= a[begin2]) { tmp[index++] = a[begin1++]; } else { tmp[index++] = a[begin2++]; } } //两组数据比较,放入tmp while (begin1 <= end1) { tmp[index++] = a[begin1++]; } while (begin2 <= end2) { tmp[index++] = a[begin2++]; } //剩下的直接放入tmp memcpy(a + i, tmp + i, sizeof(int) * (end2-i+1)); //tmp覆盖到原数组,注意,覆盖要从当前第一组数据开始覆盖,以免覆盖了随机值 } gap *= 2; printf("\n"); } free(tmp); }
4.3特性:
空间复杂度:O(N)
因为要开辟n个大小的数组
时间复杂度:O(N*logN)
稳定性:稳定
对于重复的数据,肯定是begin1那组数据在相对位置上在前面的,那么因为<=,所以会优先把begin1组的数据放进去,这样相对位置就不会变了
5.计数排序
void CountSort(int* a, int n) { int min = a[0], max = a[0]; for (int i = 0; i < n; i++) { if (a[i] > max)max = a[i]; if (a[i] < min)min = a[i]; } //先通过遍历,找到最大最小 int range = max - min + 1; //开辟range大小的数组 //range是所有出现的数据的范围 int* count = (int*)malloc(sizeof(int) * range); if (count == NULL) { perror("malloc fail"); return; } memset(count, 0, sizeof(int) * range); for (int i = 0; i < n; i++) { count[a[i] - min]++; } //通过a[i]-min的方式,让出现的数据自动计数 int j = 0; for (int i = 0; i < range; i++) { while (count[i]--) { a[j++] = i + min; } } //最后按顺序,输出count数组里相应值次数的下标+min即可。 }
5.1特性:
空间复杂度:O(range)
时间复杂度:O(range/N)
因为可能出现,数据不集中,那么这时候range是大于N,那么遍历的次数来说,时间复杂度肯定是以range为主,相反,如果数据很集中,可能range比N小很多,那么时间复杂度就是
N为主
稳定性:稳定。
本质上没有排序,只是按顺序输出数组值次罢了,