前言
排序方式是真的很多...
学校里只接触了冒泡,还觉得冒泡挺难...
学了之后感觉还有好多好难得排序...
基于比较的排序
时间复杂度O(n^2), 空间复杂度O(1)的算法
感觉这种算法大多的特点都是 需要遍历 且遍历中还需要遍历...
时间复杂度太高,不适合处理大批量数据
选择排序
思路:每一次从待排序的数据元素中选取最小(或最大)的一个元素作为首元素,直到所有元素排序完成。
比如 这样, 7 5 9 2 8
第一次循环,找出最小值2,让2位于0位置
第二次循环,找出次小值5,让5位于1位置...
保证数组中,每个最小的数字都在数组的最左侧
void selectSort(int arr[], int n){
if(arr ==NULL){
printf("Error: the arr is empty!");
return;
}
int i,j, min;
for(i=0; i< n-1; i++){ //外层循环用于遍历数组
min = i;
for(j= i+1; j<n; j++){ //内层循环用于找到最小元素
if(arr[j] <arr[min]){ // 如果当前遍历到的元素小于最小值 就把二者的下标交换
min = j;
}
int temp = arr[min]; // 交换值
arr[min] = arr[i];
arr[i] = temp;
}
}
冒泡排序
冒泡排序的思路就是
遍历数组
相邻的两个元素比较, 如果前者大于后者,就换位置
比如
7 5 9 2 8
第一轮换 5 7 9 2 8 -》5 7 2 9 8-》 5 7 2 8 9
这时可以保证,数组最右边的数字一定是整个数组中最大的值,下次只需要从 0位置遍历到 n-1位置就可以
void bubSort(int arr[], int n){
if(arr ==NULL){
printf("Error: the arr is empty!");
return;
}
int i, j;
for(i= 0; i< n-1; i++){ // 相当于设定遍历多少次
for(j = 0; j<n-i-1; j++){ // 整个数组遍历并交换
if(arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
插入排序
思路:
将未排序的元素插入到已经有序的序列中去。
比如
7 5 9 2 8
在第一轮循环中,只有7被看作有序部分, 5 9 2 8无序
在第二轮循环中,无序中第一个数字变成有序的 5 7部分,9 2 8仍无序
第三轮中,5 7 9...
void insertSort(int arr[], int n){
if(arr == NULL){
printf("Error: the arr is empty!");
return;
}
int i, j, key;
for(i=1; i<n; i++){
key = arr[i];
j = i-1;
while(j >=0 && arr[j] >key){
arr[j+1] = arr[j];
j--;
}
}
}
时间复杂度O(n logn),空间复杂度O(logn)的算法
快速排序
常见的不基于比较的排序中只有一个符合这个要求:快速排序
快速排序前的partition
研究快速排序前,要先研究partition
partition的思路是这样的:
有这样一组数:7 5 9 2 8
想实现这样一件事:小于7的所有值放在7的左侧,大于7的放在7的右侧
实现思路:先设定一个小于等于区,右边界设为 k=- 1
a[0]先和num比较
if a[i]> num{
i++;} a[0]比num大,直接跳过
a[0]比num小 让a[i]和小于等于区的最近的一个数 a[k+1] 再让右边界 k++
跳到越界就停
这样排下来就实现了遍历一遍
基本上是双指针的思想
void partition(int arr[], int n, int num){
if(arr == NULL){
printf("Error: the arr is empty!");
return;
}
int k=-1, i=0;
while(i < n){ // 遍历整个数组
if(arr[i] <= num){ // 如果arr[i]小于设定的num值
k++;
int temp = arr[i]; //二者做值的交换
arr[i] = arr[k];
arr[k] = temp;
} //如果arr[i]大于num,不需要操作
i++; //继续往下遍历
}
}
partition实现的荷兰国旗划分问题
荷兰国旗划分问题,简单地说就是将一个数组划分成三个部分:
小于区、等于区、大于区
7 5 9 2 8中, 如果设定num =7
可能实现出的效果就是
5 2 | 7 | 8 9
A B C
这样三个区
实现思路:
设置i记录数组位置, 设置l做左边界值 r做右边界值
1.如果a[i]== num, 应该分配在中间区域 就直接i++;
2.如果a[i]<num , 和小于等于区最右边的数做交换 a[i] = a[l+1] 然后l++;
3.如果a[i]>num, a[i] = a[r-1], 但是i不动(因为i还没遍历到r-1位置, 所以需要再验证一次)
i和r位置重合 循环停止
void helanguoqi(int arr[], int n, int num){
if(arr == NULL){
printf("Error: the arr is empty!");
return;
}
int i =0, left = -1, right = n-1;
while(i < right){
if(arr[i] ==num){
left++;
int temp = arr[i];
arr[i] = arr[left];
arr[left] = temp;
i++;
}
else if(arr[i] > num){
int temp = arr[i];
arr[i] = arr[right];
arr[right] = temp;
right--;
}
else{ i++;
}
}
}
由上述partition思想引发的排序方法:快速排序
还是用7 5 9 2 8举例
经过partition后,假设num设成了7
5 2 | 7 | 8 9
现在分成了三个区域
5 2是小于num区
7是等于num区
8 9是大于num区
那如果把 5 2看成一个整体 , 7看成一个整体 ,8 9看成一个整体,其实整个数组已经是有序的了
(5,2)<(7)<(8, 9)
但是现在再拆分来看,5,2 这明显是仍然无序的。而且8, 9 也可能是9, 8,不一定有序。
重新把这三个分区都看成一个新的整体来做partition。
5,2,把2当作num来partition
2的小于区是null, 等于区是2, 大于区是5
再这样依次递归....就能实现序列有序
但是有一个问题,假设第一次我选择的数字不是7呢?
就是说,我在两眼一摸黑的时候,或是在十万个数字我看都看不过来的时候,我怎么能这么精确的选中一个中数?显然不可能。
而且有一种极端情况:我每次选的值都是最大值或是最小值,那这种快排就会变得像前面几种排序一样,时间复杂度O(n^2)。
在这种顾虑下,优化产生了:随机快排。
为什么随机
当然是尽量防止每次选到数组中的最值,递归那么多次,每次的num(分界值)都随机,如果这还每次都选到最值的话,建议出门买彩票。
coding!
//先把上面的荷兰国旗分法复制粘贴下来
void partition2(int arr[], int n, int num){
if(arr == NULL){
printf("Error: the arr is empty!");
return 0;
}
int i=0, left = -1, right = n-1;
while(i < right){
if( arr[i] == num){
left++;
int temp = arr[i];
arr[i] = arr[left];
arr[left] = temp;
i++;
}
else if(arr[i] > num){
int temp = arr[i];
arr[i] = arr[right];
arr[right] = temp;
right--;
}
else{
i++;
}
}
}
void quickSort(int arr[], int n){
if(n <= 1){
printf("Error: the arr is empty!");
return;
}
srand(time(NULL)); //设置随机种子数
int num = arr[rand() % n]; // num为数组中的随机数
partition2(arr, n, num);
int i;
for(i = 0; i < n; i++){
if(arr[i] == num){
break; // 找到基准数的索引
}
}
quickSort(arr, i); // 递归地对左侧子数组进行排序
quickSort(arr+i+1, n-i-1); // 递归地对右侧子数组进行排序
}
时间复杂度O(n logn), 空间复杂度 O(n)的算法
归并排序
归并排序的特点是:无穷无尽的递归
归并本身的代码并不复杂
甚至说看见了以为写错了..
void mergeSort(int arr[], int L, int R){ //L是左,R是右
if(L== R){
return;
}
int mid= L+ ((R- L) >>1); // (R-L)>>1是右移一位,二进制中相当于除以二
mergeSort(arr, L, mid); // 看起来很明确 左半边排序
mergeSort(arr, mid+1, R); // 右半边排序
merge(arr, L, mid, R); // 合并
}
Sort本身不难,关键在于merge怎么做
比如这样
1 3 5 7 9 | 0 2 4 6 8
两边都已经有序了,怎么merge才能merge出0 1 2 3 4 5 6 7 8 9?
归并思路
思路大概是这样
1 2 3 5 | 2 4 6
^ ^
设置help数组 用于copy
1.开始时,左右两侧的指针都指在最左侧
而help[0]位置, copy较小的那个数字
所以此时help = [0]
2.左边指针此时右移
1 2 3 5 | 2 4 6
^ ^
这时候,两边相等,优先copy左侧的2 这时指针再次右移...
3. 重复,直到某一侧的指针越界
coding!
这样就能实现把两个有序的数组变成一个有序的数组
代码
void merge(int arr[], int L, int mid, int R){
// 这里面的L是左边界,mid是中点,R是右边界
int* help = (int*)malloc((R-L+1)* sizeof(int)); //开一个辅助数组
int p1= L, p2= mid+1; //定义一下左右指针分别在哪
while(p1< mid && p2< R){ //两侧都没越界时
// 哪边小就copy哪边 而且相等的话优先copy左边
help[i++] = arr[p1] <= arr[p2] ? arr[p1++]: arr[p2++]
}
while(p1 <=mid){ //这边的潜台词就是,右侧已经越界了,那直接全copy走就可以
help[i++] = arr[p1++];
}
while(p2 <= R){
help[i++] = arr[p2++];
}
for(i= 0; i< R-L+1; i++){ //把help数组倒回原数组
arr[L+ i] = help[i];
}
free(help);
}
时间复杂度O(n logn), 空间复杂度 O(1)的算法
常见的基于比较的排序中,符合条件的有希尔排序和堆排序(希尔排序不会,我直接开摆
堆排序
在了解堆排序之前,肯定要先了解什么叫“堆”。
在了解堆之前,就得了解什么是完全二叉树
完全二叉树
完全二叉树,我的理解就是那些 “ 一定满足头、左、右这个顺序优先‘存在’的二叉树”
举个栗子
1
2 3
4 5 6
这就是完全二叉树,因为满足这个次序
而如果变成
1
2 3
4 5 6
3有右树却没有左树,这就很明显不是完全二叉树。
堆, 一定是完全二叉树
大根堆和小根堆
何为大根堆,何为小根堆
大根堆顾名思义:根一定比子树大的堆
上面的
1
2 3
4 5 6
就需要变成
6
5 3
4 2 1
那具体操作思路呢?
从最下层节点开始,向上遍历。应该是先左后右的顺序(因为如果存在右一定存在左,存在左不一定存在右)
那先写一个堆的交换函数,然后递归的方式往上遍历
coding!
void heapChange(int arr[], int i, int limit){
if(arr == NULL){
printf("Error: the arr is empty!");
return;
}
int temp;
int largest = i; // 从0开始时,子树和父节点的下标关系
int left = 2*i +1;
int right = 2 * i +2;
if(left <limit && arr[left]> arr[largest]){ //如果左边子树没越界 且左子树大于父树
largest = left; // 下标交换 此时left是最大那棵树
}
if(right< limit && arr[right] > arr[largest]){
largest = right;
}
if(largest != i){ // 如果largest不是原本那个父树了
temp = arr[i]; // 交换“原父树”和新的largest
arr[i] = arr[largest]; // 让新的largest变成新父树
arr[largest] = temp;
heapchange(arr, largest, limit);
}
}
void bigHeap(int arr[], int limit){
int i;
for(i = limit/2 - 1; i>=0; i--){ //从下往上遍历,时间复杂度更低
heapchange(arr, i, limit);
}
}
小根堆
小根堆几乎等于大根堆,只需要调整一些位置,代码在下面的堆排序中实现吧~
堆排序
大根堆实现了父树永远大于子树,这在某种意义上来说,也是一种排序(父>子)
在无穷无尽的递归下,也可以实现排序效果,coding!
void heapChange(int arr[], int i, int limit){ //小根堆的交换法则
if(arr == NULL){
printf("Error: the arr is empty!");
return;
}
int min = i, left = 2*i +1, right = 2*i +2; //先定义好各个位置
if(left<lmit && arr[left]< arr[min]){
left = min;
}
if(right < limit && arr[right] < arr[min]{
right = min;
}
if(min != i){
int temp = arr[i]; // 交换“原父树”和新的largest
arr[i] = arr[min]; // 让新的largest变成新父树
arr[min] = temp;
heapchange(arr, min, limit);
}
}
void minHeap(int arr[], int limit){ // 造小根堆的函数
int i;
for(i = limit/2 - 1; i>=0; i--){ //从下往上遍历,时间复杂度更低
heapchange(arr, i, limit);
}
}
void heapSort(int arr[], int n) { //堆排序
int i;
minHeap(arr, n); // 先将数组建成小根堆
for (i = n - 1; i > 0; i--) {
int temp = arr[0]; // 将堆顶元素与最后一个元素交换位置
arr[0] = arr[i];
arr[i] = temp;
heapChange(arr, 0, i); // 对交换后的堆顶元素进行维护
}
}
deal!
不基于比较的排序
不基于比较的排序有一个共同特点:都对数据本身有严格要求。但是同时,他们的时间复杂度只有O(n+k)
计数排序
比如一家公司的员工,年龄应该都是0-200岁/手动狗头
操作是:
建立一个help数组(大小为 200-0+1)
这时候我的help数组是由201个0构成的
开始遍历,如果第一个是1岁的员工 就在arr[1]位置++
此时help数组变成 0100000000000......
遍历之后,比如数组前几位是 3 2 5 1 2
对应位置分别是 0 1 2 3 4
排序就排出来了: 000 11 22222 3 44.......and so on
coding也很简单
void countSort(int arr[], int n, int min, int max){// min和max为了让数据有个输入范围
int* help = (int*)malloc((max-min+1) * sizeof(int));
for(int i= 0; i< n; i++){
int temp = arr[i]- min;
help[temp]++;
}
// 累加计数
for (int i = 1; i <= max-min; i++) {
help[i] += help[i-1];
}
// 排序到输出数组中
int* output = (int*) malloc(n * sizeof(int));
for (int i = n-1; i >= 0; i--) {
int temp = arr[i] - min;
output[help[temp]-1] = arr[i];
help[temp]--;
}
// 将排序好的数组复制到原始数组中
for (int i = 0; i < n; i++) {
arr[i] = output[i];
}
// 释放内存
free(help);
free(output);
}
基数排序
需要数据十进制非负
简单来说...就是依次排序
17 100 101 85 3
这样几个数
首先,最高位次是3位,所以不够三位的全补0
017 100 101 085 003
准备十个队列,按个位数入队
0 1 2 3 4 5 6 7 8 9
100 101 003 085 017
从左到右依次取出来,新顺序变成
100 101 003 085 017
再根据十位数字入队
0 1 2 3 4 5 6 7 8 9
100 017 085
101
003
从左到右依次取出来,新顺序变成
100 101 003 017 085
再根据百位数字...
0 1 2 3 4 5 6 7 8 9
003 100
017 101
085
取出来...
003 017 085 100 101
deal!
仔细看看,有点类似于 先比个位,再比十位,再比百位
coding!
int getMax(int arr[], int n) {
int max = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > max) {
max = arr[i]; // 寻找最大值
}
}
int digits = 0;
while (max > 0) {
digits++; //看最大值有几位并返回
max /= 10;
}
return digits;
}
// 基数排序函数
void radixSort(int arr[], int n) {
// 获取数列中最大值的位数
int digits = getMax(arr, n);
// 定义桶数组
int bucket[10][n];
// 定义桶中的元素数量
int bucketCount[10];
// 初始化桶中元素数量为0
for (int i = 0; i < 10; i++) {
bucketCount[i] = 0;
}
// 循环遍历每一位数值
for (int i = 1; i <= digits; i++) {
// 将每个元素分配到对应的桶中
for (int j = 0; j < n; j++) {
int digit = (arr[j] / (int)(pow(10, i - 1))) % 10;
bucket[digit][bucketCount[digit]] = arr[j];
bucketCount[digit]++;
}
// 将桶中的元素按顺序取出,更新原始数组
int index = 0;
for (int j = 0; j < 10; j++) {
for (int k = 0; k < bucketCount[j]; k++) {
arr[index] = bucket[j][k];
index++;
}
// 将桶中元素数量重置为0
bucketCount[j] = 0;
}
}
}
排序的稳定性
以下几个数排序
01010202
所有排序方式都能排出来 00001122
问题在于:这个0还是原来那个0吗?这个1还是原来那个1吗?
给之前几个数加个标记
0 1 0 1 0 2 0 2
a b c d
先只看0
如果排序出来的0还是按照abcd的顺序的话,就是稳定的
如果改变了次序的话,就是不稳定的...
定义就是: 如果不因排序而改变相对次序,这个排序就是稳定的
总结
基于比较 | 时间复杂度 | 空间复杂度 | 稳定性 |
选择 | O(n^2) | O(1) | 无 |
冒泡 | O(n^2) | O(1) | 有 |
插入 | O(n^2) | O(1) | 有 |
归并 | O(n logn) | O(n) | 有 |
快排 | O(n logn) | O(log N) | 无 |
堆排 | O(n logn) | O(1) | 无 |
最低时间复杂度为 O(n logn)
时间复杂度做到O(n logn)且空间复杂度低于O(n)的排序,不可能稳定
要求稳定性用归并排序
要求速度用随机快排
要求空间复杂度低用堆排