排序算法
1.冒泡排序
冒泡排序:两两之间互换比较交换,比较一趟能确定一个数的位置。假设有n个数,则需要比较n-1趟,就能确定每一个数的位置,第一趟下标范围为[0,n-2],第二趟下标范围为[0,n-3],...则可以写成代码。
c
void BubbleSort(int* a,int n)
{
for (int i = n-1; i > 0; i--)
{
int flag = 1;
for (int j = 0; j < i; j++)
{
if (a[j] > a[j + 1])
{
flag = 0;
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
}
}
if (flag)
break;
}
}

2.插入排序

先写排好一个数字,假设[0,end]是一个有序数组,则下一个数用tmp保存,tmp =a[end+1],如果tmp<a[end],a[end]的数移到a[end+1],如果tmp>a[end],把tmp放到下标为end的位置。
由此看来,end是有序数组最后一个数的下标,tmp保存的是下一个要放的数,一个无序数组,则end先指向第一个数,tmp保存第二个数,根据上述获得这两个数的正确排序,则end = n-2,tmp = a[end-1]时,进行最后一次排序,整个数组就排好序了。
c
//O(N) = O(N^2)
void InsertSort(int* a,int n)
{
//
//[0,end] end+1
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;
}
}
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
3.希尔排序
下面分的间隔gap=3,分成3组。
希尔排序在插入排序的基础上升级了一点,把一个无序数据按gap(间隔)分成gap个数组,对这gap个数组分别进行插入排序,先把gap的值取大一点,把一个组小的数排到前面,大的数排到后面,gap进行不断变化取值,当gap = 1时,就是插入排序,排序完成。
先进行排第一个组排序如下。。
c
//[0,end]有序 tmp保存下一个数
int gap = 3; //把一个数组分成3个数组
for(int i = 0;i<n-gap;i+=gap)
{
int end= i;
int tmp = a[end+gap];
while(end>=0)
{
if(tmp<a[end])
{
a[end+gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end+gap]= tmp;
}
对gap组数组进行排序,并且gap取值变化,当gap = 1时,完成排序。
c
void ShellSort(int*a ,int n)
{
int gap = n;
while(gap>0)
{
//[0,end]有序 tmp保存下一个数
gap /= 2; //把一个数组分成3个数组
for(int j = 0;j<gap;j++)
{
for(int i = j;i<n-gap;i+=gap)
{
int end= i;
int tmp = a[end+gap];
while(end>=0)
{
if(tmp<a[end])
{
a[end+gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end+gap]= tmp;
}
}
}
}
时间复杂度:O(N) = N^(1.3)
大致分析如下,不会算!
按照gap = n,gap = gap/3来大致计算时间复杂度。
第一次分组,gap = n/3,则把n个数据排成n/3个数组,每个数组的间隔为gap,每个数组的大小为3个数据,每个数组排序最坏情况移动3次,则移动次数为:3*(n/3) = n次。
... ... ... ... ...
最后一次,gap = 1,数组经过多轮预排之后,已经非常接近有序,大致移动n次就可以使数组有序。
4.选择排序
先确定区间[begin,end],然后再这个区间找最大值,最小值,进行交换,begin++,end- -,缩小区间。
c
void swap(int*px,int*py)
{
int tmp =*px;
*px = *py;
*py = tmp;
}
void SelectSort(int*a,int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin, maxi = begin;
for (int i = begin + 1; i <= end; i++)
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
swap(&a[begin], &a[mini]);
if (maxi == begin)
{
maxi = mini;
}
swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
5.快排
hoare版本。
思路:快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

取key永远为子序列最左边的一个数,定义left指向子序列最左边,right指向子序列最右边,先走最右边,找比key小的值,在走左边,找比keyi大的值,找到之后,两者交换,交换之后,比key小的在左边,比key大的在右边。left与right相遇则结束,然后key与相遇位置的数交换,key已经放到它应有的位置,分割区间[begin,keyi-1][keyi][keyi+1,end],进行递归遍历,放好每一个数。
具体过程如下:
通过写这个算法,明白还是提高代码组织能力,一个算法有了思路,怎么用代码实现的问题。
c
void QuickSort(int* a, int left,int right)
{
if (left >= right)
return;
int begin = left;
int end = right;
int keyi = left;
while (left < right)
{
//先走右边,右边找比a[key]小的
while (left < right && a[keyi] >= a[right])
{
right--;
}
//在走左边,左边找比a[key]大的
while (left < right && a[keyi] <= a[left])
{
left++;
}
swap(&a[left], &a[right]);
}
swap(&a[keyi], &a[left]);
keyi = left;
//[begin,keyi-1]keyi[keyi+1,end]
QuickSort(a, begin,keyi-1);
QuickSort(a, keyi+1,end);
}
理解为啥相遇位置的值比key小?
时间复杂度分析:
时间复杂度一般情况下O(N) = N* log 2 ( N ) \log_{2}(N) log2(N)
最坏情况下:为有序,O(N) = N^2.
快排优化,快排时间复杂度高的原因是数据有序,第一个元素是最小的。
第一种方式随机数选keyi,使得第一个数不是最小的(升序)。具体如下。
int randi = rand()%(right-left+1)+left;
swap(&a[left],&a[randi]);
其实上面这种优化,我觉得还是不靠谱,你怎么能保证你随机选的keyi就不是最小值呢?
c
void QuickSort(int*a ,int left,int right)
{
if(left>=right)
return;
int begin = left,end = right;
//随机数选keyi
int randi = rand()%(right-left+1)+left;
Swap(&a[left],&a[randi]);
int keyi = left;
while(left<right)
{
//先走右边,找比a[keyi]小的值
while(left<right && a[keyi]<=a[right])
{
right--;
}
//在走左边,找比a[keyi]大的值
while(left<right && a[keyi]>=a[left])
{
left++;
}
Swap(&a[left],&a[right]);
}
Swap(&a[keyi],&a[left]);
keyi = left;
//[begin,keyi-1] keyi [keyi+1,end]
QuickSort(a,begin,keyi-1);
QuickSort(a,keyi+1,end);
}
第二种优化方式三数取中放keyi位置,这个方法比上面好。定义int GetMid(int *a,int left,int right)取每个区间的最左边值和最右边值,中间值,算出大小在中间的值做keyi。
c
//三数取中,left,mid,right
//大小居中的值,也就是不是最大也不是最小的
int GetMidi(int* a, int left, int right)
{
int midi = (left + right) / 2;
if (a[left] < a[midi])
{
if (a[midi] < a[right])
{
return midi;
}
else if (a[left] > a[right]) //a[midi]>a[right]
{
return left;
}
else
{
return right;
}
}
else //a[left]>a[midi]
{
if (a[midi]>a[right])
{
return midi;
}
else if (a[left] < a[right]) //a[midi]<a[right]
{
return left;
}
else
{
return right;
}
}
}
void QuickSort(int*a ,int left,int right)
{
if(left>=right)
return;
int begin = left,end = right;
//随机数选keyi
// int randi = rand()%(right-left+1)+left;
// Swap(&a[left],&a[randi]);
int midi = GetMidi(a,left,right);
Swap(&a[left],&a[midi]);
int keyi = left;
while(left<right)
{
//先走右边,找比a[keyi]小的值
while(left<right && a[keyi]<=a[right])
{
right--;
}
//在走左边,找比a[keyi]大的值
while(left<right && a[keyi]>=a[left])
{
left++;
}
Swap(&a[left],&a[right]);
}
Swap(&a[keyi],&a[left]);
keyi = left;
//[begin,keyi-1] keyi [keyi+1,end]
QuickSort(a,begin,keyi-1);
QuickSort(a,keyi+1,end);
}
第三种小区间优化,剩余10个数直接进行插入排序。这种写法递归下去之后在<10个数的区间,直接进行插入排序,不需要返回条件,程序执行完自动返回。
c
void QuickSort(int*a ,int left,int right)
{
if(right-left+1<10)
{
InsertSort(a+left,right-left+1);
}
else
{
int begin = left,end = right;
int keyi = left;
while(left<right)
{
//先走右边,找比a[keyi]小的值
while(left<right && a[keyi]<=a[right])
{
right--;
}
//在走左边,找比a[keyi]大的值
while(left<right && a[keyi]>=a[left])
{
left++;
}
Swap(&a[left],&a[right]);
}
Swap(&a[keyi],&a[left]);
keyi = left;
//[begin,keyi-1] keyi [keyi+1,end]
QuickSort(a,begin,keyi-1);
QuickSort(a,keyi+1,end);
}
}
第二种写快排的方法:前后指针法。
定义两个指针,prev指向第一个数,cur指向第二个数,如果a[cur]<key,则prev++,然后prev与cur交换,在cur++,如果a[cur]>=key,则cur++,最后cur++出界,循环结束,prev的位置就是key值的位置,至于为什么这么操作,就把key值位置找到了,我也不知道。
c
void QuickSort(int*a,int left,int right)
{
if(left>=right)
return;
int prev = left;
int cur = left+1;
int keyi =left;
while(cur<=right)
{
if(a[cur]<a[keyi])
{
++prev;
Swap(&a[cur],&a[prev]);
++cur;
}
else{
++cur;
}
}
Swap(&a[keyi],&a[prev]);
keyi = prev;
//[left,keyi-1]keyi[keyi+1,righgt]
QuickSort(a,left,keyi-1);
QuickSort(a,keyi+1,right);
}
模拟递归过程,使用栈来实现快排。
模拟递归过程,写快排的关键是搞区间,区间确定了,搞一个单趟排就可以了,先把第一个区间[lefe,right]入栈,再走一个单趟排,则一个数字位置已确定,并且分割出两个区间[begin,keyi-1]keyi[keyi+1,end],两个区间在走个单趟,依次循环。
c
void QuickSortNonR(int*a,int left,int right)
{
ST st;
STInit(&st);
STPush(&st,right);
STPush(&st,left);
while(!STEmpty(&st))
{
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
//先把一趟排好
int prev = begin;
int cur = begin+1;
int keyi =begin;
while(cur<=right)
{
if(a[cur]<a[keyi])
{
++prev;
Swap(&a[cur],&a[prev]);
++cur;
}
else{
++cur;
}
}
Swap(&a[keyi],&a[prev]);
keyi = prev;
//[begin,keyi-1] keyi [keyi+1,end]
if(keyi+1<end)
{
STPush(&st,end);
STPush(&st,keyi+1);
}
if(begin<keyi-1)
{
STPush(&st,keyi-1);
STPush(&st,begin);
}
}
STDestory(&st);
}
6.归并排序
基本思想:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
思路:归并排序是走的二叉树的后序,而快排是走的二叉树的前序,归并排序是先把左子树、右子树、遍历到底部,只剩下一个数,返回时,进行比较,进而进行归并。
c
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin == end)
return;
//后序来搞,分区间
int mid = (begin + end) / 2;
//[begin,mid][mid+1,end]
printf("[%d,%d]-[%d,%d]\n", begin, mid, mid + 1, end);
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
//大小处理,两个区间处理排序
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
PrintArray(a + begin, end - begin + 1);
}
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);
}
时间复杂度:O(N) = N* log 2 ( N ) \log_{2}(N) log2(N)
归并排序循环实现
一点点写这个算法,慢慢来感受怎么组织程序。
怎么弄呢?递归实现归并排序,用的是后序,那么通过循环实现归并,则要从正面出发来搞这个排序。
首先把数组中的每一个数据看作是一个数组,也就是分割,gap = 1来分割,分割之后,两个数组之间进行排序,每两个一组排好序,gap = 2,再次排序。直到完成这个排序。
先写第一次分割的两个数组的排序
c
int begin1 = 0, end1 = begin1 + gap - 1;
int begin2 = end1 + 1, end2 = begin2 + gap - 1;
int i = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a, tmp , sizeof(int) * (end2 + 1));
在写一次全部分割的排序
c
for (int j = 0; j < n; j += gap * 2)
{
int begin1 = j, end1 = begin1 + gap - 1;
int begin2 = end1 + 1, end2 = begin2 + gap - 1;
int i = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));
}
整体来写
c
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
int gap = 1;
while (gap < n)
{
for (int j = 0; j < n; j += gap * 2)
{
int begin1 = j, end1 = begin1 + gap - 1;
int begin2 = end1 + 1, end2 = begin2 + gap - 1;
int i = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
边界处理
//特殊情况矫正
//1.如果begin1越界,不用管,前面已经配对成对。
//2.如果end1越界,[begin1,end1),后面没有配对的另一个数组,不用归并
if(end1>=n)
break;
//3.如果begin2越界,[begin1,end1]已经排好序,不用归并,end1后面就没有归并的数组
if(begin1>=n)
break;
//4.如果end2越界,[begin2,end2)还有数据,一定要归并,只不过弄好数据范围
if(end2>=n)
end2 = n-1;
c
void MergeSortNonR(int*a,int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if(tmp == NULL)
{
perror("malloc fail");
return ;
}
int gap = 1;
while(gap<n)
{
for(int j = 0;j<n;j += gap*2)
{
int begin1 = j,end1 =begin1+gap-1;
int begin2 =end1+1,end2 = begin2+gap-1;
int i = begin1;
//特殊情况矫正
//1.如果begin1越界,不用管,前面已经配对成对。
//2.如果end1越界,[begin1,end1),后面没有配对的另一个数组,不用归并
if(end1>=n)
break;
//3.如果begin2越界,[begin1,end1]已经排好序,不用归并,end1后面就没有归并的数组
if(begin1>=n)
break;
//4.如果end2越界,[begin2,end2)还有数据,一定要归并,只不过弄好数据范围
if(end2>=n)
end2 = n-1;
while(begin1<=end1 && begin2 <= end2)
{
if(a[begin1]<a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while(begin1<=end1)
{
tmp[i++] = a[begin1++];
}
while(begin2<=end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a+j,tmp+j,sizeof(int)*(end2-j+1));
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
7、计数排序
算法思想:用相对映射来搞
c
//时间复杂度:O(N+range)
//空间复杂度:O(range)
void CountSort(int* a, int n)
{
int max = a[0], min = 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;
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]++;
}
//排序
int j = 0;
for (int i = 0; i <(range); i++)
{
while(count[i] > 0)
{
a[j++] = min + i;
count[i]--;
}
}
free(count);
count = NULL;
}
8、各个排序算法比较情况

完结!