排序在我们学习算法中,是十分重要的。现在我们就对初阶数据结构的算法进行一个总结。
一.插入排序
插入排序是在时间复杂度为o(n^2)中,最快的一个。其思想就是,在数组中从左往右开始,确定每一个数,在其前面所在的位置,遍历完数组后,即可实现排序。
由于思路比较简单,这边就直接上代码了:
void InsertSort(int* a, int n)
{
for (int i = 0; i < n; ++i)
{
int j = i;
while (j>0)
{
if (a[j] < a[j - 1])
{
swap(&a[j], &a[j - 1]);
j--;
}
else
break;
}
}
}
其实插入排序只是一个前菜,真正的插入排序plus是希尔排序,希尔排序是在插入排序的基础上,分组进行插入排序,通过预处理,来实现时间复杂度较低的排序。
我们来分析下面一段代码:
// 希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
while(gap>1)
{
gap = gap / 3+1;
for (int i = 0; i < n; i++)
{
int j = i;
while (j > gap-1)
{
if (a[j] < a[j - gap])
{
swap(&a[j], &a[j - gap]);
j-=gap;
}
else
break;
}
}
}
}
gap是希尔排序中,每段插入排序的间隔,明白了这个点,那就好分析了。
gap会随着排序不断减小,直到1,但是经过多轮的预排序后,最后一轮的插入排序,时间复杂度会很低。这就是希尔排序,时间复杂度大概是o(n^1.3)。
二.选择排序
第二种是选择排序,顾名思义,就是在整个数组中,选出最大或者最小的,放在数组最前面,或者最后面,在确定好相对最小最大值后,再依次往后遍历。
话不多说,直接上代码:
//选择排序
void SelectSort(int* a, int n)
{
for (int i = 0; i < n; ++i)
{
int mini = i;
for (int j = i; j < n; ++j)
{
if (a[mini] > a[j])
mini = j;
}
swap(&a[i], &a[mini]);
}
}
当然,如果只是它分为一类,那也太low了,其实,我们之前提到的堆排序也是选择排序的一种,它通过堆顶元素和最后一个元素交换,再整理堆,重复这个过程,就可以实现升序或者降序。
向上向下调整法在这里就不多说了,具体的在上一篇博客可以看到。
void HeapSort(int* a, int n)
{
for (int i = 0; i < n; ++i)
{
AdjustUp(a, i);
}
for (int i = n-1; i >=0; --i)
{
swap(&a[i],&a[0]);
AdjustDwon(a, i, 0);
}
}
通过这个方法也可以实现排序,并且时间复杂度只有o(nlogn)。
三.交换排序
我们知道一个很经典的交换排序,就是冒泡排序,但是呢,有一个更加出名的排序,叫做快速排序,即快排。创始人霍尔的思想是:选定一个基准值,规定left指向最左边的值,right指向最右边的值。从right向前找第一个小于基准值的数,找到后,再从left向后寻找第一个大于基准值的数,此时交换两个数后,继续从此刻right的位置向前找第一个小于基准值的数,再从left位置向后找第一个大于基准值的数,找到后,两数进行交换,重复如此,直到left和right相遇,再交换left与right相遇的值和基准值,此时,一趟快速排序便结束了,基准值也就找到了自己合适的位置,基准值的左右两边又是新的无序序列,这时只需要递归的进行快速排序即可。
实现快速排序的单趟排序其实不止一种,除了霍尔大佬的想法,还有其他的方法,这里我一共提供三种,一种是原始版:
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
int keyi = left;
while (left<right)
{
while (a[right] > a[keyi])
{
right--;
}
while (left < right&&a[left] <= a[keyi])
{
left++;
}
swap(&a[left], &a[right]);
}
swap(&a[keyi], &a[left]);
return left;
}
第二个是挖坑法,思想是,还是确定一个基准值(数组第0个元素),坑位在一开始的基准值,从右往左找到一个比基准值大的,将其跟基准值交换,并把坑位交给它。再从左往右找一个比基准值小的,再将其跟基准值交换,并把坑位交给它。重复此过程,当left>=right的时候,即可找到基准值的最终位置。
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int hole = left;
while (left < right)
{
while (a[right] > key)
{
right--;
}
a[hole] = a[right];
hole = right;
while (left<right&&a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
第三种是前后指针法,这个稍有些抽象,这个了解就好。
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int first = left;
int second = left+1;
while(second<=right)
{
if (a[second] < a[keyi])
{
++first;
swap(&a[second], &a[first]);
}
++second;
}
swap(&a[first], &a[keyi]);
return first;
}
这个就是快速排序的单趟排序的函数。
快速排序的实现一般都是用递归实现,通过前序遍历,即可得到排序。
void QuickSort(int* a, int left, int right)
{
if (right<=left)
{
return;
}
//int keyi = PartSort1(a, left, right);
//int keyi=PartSort2(a, left, right);
int keyi = PartSort3(a, left, right);
QuickSort(a, left,keyi-1);
QuickSort(a, keyi+1, right);
}
其实快速排序还有非递归版本,其需要用栈来实现,因为递归的本质是函数压栈,所以我们可以通过栈模拟递归,依旧是前序的思想,每次压栈头尾,实现了单趟排序之后,头尾出栈,再实现左右压栈。
上代码:
void QuickSortNonR(int* a, int left, int right)
{
Stack sk;
StackInit(&sk);
StackPush(&sk, left);
StackPush(&sk, right);
while (!StackEmpty(&sk))
{
int end = StackTop(&sk);
StackPop(&sk);
int begin = StackTop(&sk);
StackPop(&sk);
int keyi = PartSort3(a, begin, end);
if (begin < keyi - 1)
{
StackPush(&sk, begin);
StackPush(&sk, keyi - 1);
}
if (end > keyi+1)
{
StackPush(&sk, keyi + 1);
StackPush(&sk, end);
}
}
StackDestroy(&sk);
}
这就是所有的快速排序。
四.归并排序
接下来是归并排序,归并排序运用的也是二叉树的思想,将一段顺序表不断分成两半,知道只有一个元素,通过后序遍历,两两进行排序归并,这样就可以实现归并排序。
// 归并排序递归实现
void MergeSort(int* a, int left,int right,int *tmp)
{
if (left >= right)
return;
int mid = (right + left) / 2;
MergeSort(a,left, mid,tmp);
MergeSort(a, mid+1, right,tmp);
int begin1 = left; int end1 = mid;
int begin2 = mid + 1; int end2 = right;
int i = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i] = a[begin1];
begin1++;
}
else
{
tmp[i] = a[begin2];
begin2++;
}
i++;
}
while (begin1 <= end1)
{
tmp[i] = a[begin1];
begin1++;
i++;
}
while (begin2 <= end2)
{
tmp[i] = a[begin2];
begin2++;
i++;
}
memcpy(a + left, tmp + left , sizeof(int) * (right - left + 1));
}
可以看到,我们开辟了一个新数组tmp来进行元顺序表的改变。
当然归并排序也可以进行非递归的方法
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
int i = 0;
int* tmp = (int*)malloc(sizeof(int)*100);
int gap = 1;
while(gap<n)
{
for(i = 0;i < n;i += gap * 2)
{
int begin1 = i; int end1 = begin1 + gap - 1;
int begin2 = gap + i; int end2 = begin1 + 2 * gap - 1;
int j = i;
//一共考虑三张情况的越界。
if (end1 >= n || begin1 >= n)
break;
//修正end2
if (end2 >= n)
end2 = n - 1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j] = a[begin1];
begin1++;
}
else
{
tmp[j] = a[begin2];
begin2++;
}
j++;
}
while (begin1 <= end1)
{
tmp[j] = a[begin1];
begin1++;
j++;
}
while (begin2 <= end2)
{
tmp[j] = a[begin2];
begin2++;
j++;
}
memcpy(a+i, tmp+i, sizeof(int)*(end2-i+1));
}
gap *= 2;
}
free(tmp);
}
这里就简单了解一下,要注意,右边界越界的问题(有兴趣的朋友们可以想一想什么情况会越界,是哪种情况的越界)。
五.计数排序
计数排序在排序大家庭中可以说是一个远门亲戚了,他的思想非常之独特:开辟一个新数组,其下标是原顺序表中的元素的值,通过对新数组(我们叫做tmp)的对应下标的数进行加减,来确定原数组中,对应元素数值的个数,再将tmp对应着返回给原数组。
听起来有些抽象,我们看看代码:
// 计数排序
void CountSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(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 gap = max - min + 1;
for (int i = 0; i < gap; ++i)
{
tmp[i] = 0;
}
for (int i = 0; i < n; ++i)
{
tmp[a[i]-min]++;
}
int i = 0;
int j = 0;
while (i < n &&j < max)
{
if (tmp[j] != 0)
{
a[i] = j+min;
tmp[j]--;
++i;
}
else
j++;
}
free(tmp);
}
这个就是计数排序。
六.总结
排序对于我们后续学习算法中有着至关重要的作用,上面经典的排序也要初步掌握,如果我有哪里不正确的地方,也请大家指点一下,谢谢浏览!