在C语言的学习过程中,大家肯定接触过冒泡排序,那其实还有许多的
非常优秀的排序等待着我们去学习,今天就由我来介绍一下数据结构中的各种排序。
1.插入排序
插入排序分为两种,一种是希尔排序,首先来介绍直接插入排序,他的思想就是遍历元素,将当前遍历的元素与已遍历过的元素,一一比较,当遍历到的元素比它大时交换两者。
代码如下:
void InsertSort(int* a, int n)
{
for (int end = 1; end < n; end++)
{
int key = a[end];
int tmp = end - 1;
while (tmp >= 0)
{
if (key >= a[tmp])
{
break;
}
a[tmp + 1] = a[tmp];
tmp--;
}
a[tmp + 1] = key;
}
}
时间复杂度:O(N^2),空间复杂度O(1)。
2.希尔排序
希尔排序,是插入排序的一种变种,我们可以发现当数组有序时,插入排序的时间复杂度变成了O(N),这说明对于插入排序来说,原数据越接近有序,耗费的时间越少。那么对于希尔排序,他添加了一个元素(以下称gap),他先把间隔为gap的所有元素先排好序,然后缩小gap循环上述过程,直到gap为1来一次插入排序,而这一次的插入排序面对的数组接近有序,减少了时间的消耗。
代码如下:(和上图有点小不一样,做动画的时候做错了。。。主要是gap走几步的理解不一样)
//希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
while(gap > 1)
{
gap = gap / 3 + 1;
for(int i = 0; i + gap < n; i++)
{
for (int end = i + gap; end < n; end += gap)
{
int key = a[end];
int tmp = end - gap;
while (tmp >= 0)
{
if (key >= a[tmp])
{
break;
}
a[tmp + gap] = a[tmp];
tmp -= gap;
}
a[tmp + gap] = key;
}
}
}
}
这段代码我们看到,嵌套了很多层循环,其实可以优化,代码如下:
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i + gap< n; i++)
{
int end = i + gap;
int key = a[end];
int tmp = end - gap;
while (tmp >= 0)
{
if (key >= a[tmp])
{
break;
}
a[tmp + gap] = a[tmp];
tmp -= gap;
}
a[tmp + gap] = key;
}
}
}
时间复杂度:O(N^1.3),空间复杂度:O(1)。
gap每次的值是不明确规定的(除以三,除以合理的都可以),但最后一次一定是直接插入排序,所以要在后面加一。
关于希尔排序的时间复杂度计算很复杂,这里直接给出大致的结果。有兴趣可以自己去看看应该如何计算。
3.选择排序
选择排序的思想很简单就是遍历数组,将数组的最小值记录下来,并与当前位置交换。
代码如下:
void OptionSort(int* a, int n)
{
for (int i = 0; i < n; i++)
{
int mini = i;
for (int j = i + 1; j < n; j++)
{
if (a[j] < a[mini])
{
mini = j;
}
}
Swap(&a[i], &a[mini]);
}
}
时间复杂度:O(N^2),空间复杂度:O(1)。
4.堆排序
5.冒泡排序
这个就不再赘述了。
6.快速排序
快速排序,它的原理是选取数组中的一个元素来作为一个标准值(一般是选取最左边的元素),将大于他的移向他的右边,小于他的移向他的左边,然后多趟递归他的左边和右边,直到区间中的元素小于等于一个,结束递归。
注:以下区间都是闭区间,是为了更好的把握区间的变换,不明确规定。
代码如下:
1:第一种单趟的方法是提出快速排序的人霍尔所提出的方法,先让右面找到小于标准值的,再让左边找到大于标准值的(确保出循环后,left指向的一定是小于key的最右边那个元素),然后交换,再循环。
int _QuickSort1(int* a, int begin, int end)
{
int keyi = begin;
int left = begin;
int right = end;
while (left < right)
{
while (left < right && a[keyi] <= a[right])
{
right--;
}
while (left < right && a[keyi] >= a[left])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = _QuickSort1(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi +1, end);
}
2.挖坑法:
int _QuickSort2(int* a, int begin, int end)
{
int holei = begin;
int key = a[begin];
int left = begin;
int right = end;
while (left < right)
{
while (left < right && key <= a[right])
{
right--;
}
a[holei] = a[right];
holei = right;
while (left < right && key >= a[left])
{
left++;
}
a[holei] = a[left];
holei = left;
}
a[holei] = key;
return holei;
}
3.双指针:
int _QuickSort3(int* a, int begin, int end)
{
int prev = begin;
int cur = begin + 1;
int keyi = begin;
while (cur <= end)
{
if (a[cur] < a[keyi])
{
++prev;
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[keyi], &a[prev]);
keyi = prev;
return keyi;
}
代码优化:
关于上面的代码,有个缺陷,就是在选最左边的值为key的过程中有可能会每次都选的key较小,导致递归的区间不够平分,导致其时间会大量增加,例如原数组在有序的情况下,会导致快速排序的时间复杂度提升到O(N^2),所以我们可以对于选key要稍加修饰。新增MidI函数,为了找到数组中相对‘中间值’的数。
代码如下;
int MidI(int* a, int begin, int end)
{
int midi = (end - begin) / 2;
if (a[midi] > a[begin])
{
if (a[midi] < a[end])
return midi;
else
{
if (a[begin] > a[end])
return begin;
else
return end;
}
}
else
{
if (a[begin] < a[end])
return begin;
else
{
if (a[midi] > a[end])
return midi;
else
return end;
}
}
}
int _QuickSort1(int* a, int begin, int end)
{
int midi = MidI(a, begin, end);
Swap(&a[begin], &a[midi]);
int keyi = begin;
int left = begin;
int right = end;
while (left < right)
{
while (left < right && a[keyi] <= a[right])
{
right--;
}
while (left < right && a[keyi] >= a[left])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
int _QuickSort2(int* a, int begin, int end)
{
int midi = MidI(a, begin, end);
Swap(&a[begin], &a[midi]);
int holei = begin;
int key = a[begin];
int left = begin;
int right = end;
while (left < right)
{
while (left < right && key <= a[right])
{
right--;
}
a[holei] = a[right];
holei = right;
while (left < right && key >= a[left])
{
left++;
}
a[holei] = a[left];
holei = left;
}
a[holei] = key;
return holei;
}
int _QuickSort3(int* a, int begin, int end)
{
int midi = MidI(a, begin, end);
Swap(&a[begin], &a[midi]);
int prev = begin;
int cur = begin + 1;
int keyi = begin;
while (cur <= end)
{
if (a[cur] < a[keyi])
{
++prev;
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[keyi], &a[prev]);
keyi = prev;
return keyi;
}
但是就算有了,上面那个函数,还有一种情况无法处理,那就是原数组都是相同值的情况,这样也会导致快排的时间复杂度上升到O(N^2)。所以我们又进行了优化。在上面三个函数中,都是把比key大的放到他的右边,小的放到他的左边只有两个选择,现在我们增加第三个选择,就是再挑选出等于他的,单趟排好序后只需要递归等于他前面的部分,和等于他后面的部分即可。
代码如下:
void QuickSort_ThreeRoutes(int* a, int begin, int end)
{
if (begin >= end)
return;
int midi = MidI(a, begin, end);
Swap(&a[begin], &a[midi]);
int keyi = begin;
int cur = begin;
int left = begin;
int right = end;
while (cur <= right)
{
if (a[cur] < a[keyi])
{
left++;
Swap(&a[cur], &a[left]);
cur++;
}
else if (a[cur] == a[keyi])
{
cur++;
}
else
{
Swap(&a[cur], &a[right]);
right--;
}
}
Swap(&a[keyi], &a[left]);
QuickSort_ThreeRoutes(a, begin, left - 1);
QuickSort_ThreeRoutes(a, cur, end);
}
接下来要介绍的是快排的非递归写法,这里需要用到数据结构—栈。将原数组区间压入栈(左下标和右下标成对压入),排好序后出栈,再压入要排的子序列。
代码如下:
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top; // 栈顶
int capacity; // 容量
}Stack;
void StackInit(Stack* ps)
{
ps->a = NULL;
ps->capacity = 0;
ps->top = 0;
}
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
if (ps->capacity == ps->top)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity = newcapacity;
}
ps->a[ps->top] = data;
ps->top++;
}
void StackPop(Stack* ps)
{
assert(ps);
assert(ps->top > 0);
ps->top--;
}
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->a[ps->top - 1];
}
int StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
int StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == 0;
}
void StackDestroy(Stack* ps)
{
free(ps->a);
ps->a = NULL;
ps->capacity = ps->top = 0;
}
void QuickSortNonR(int* a, int begin, int end)
{
Stack st;
StackInit(&st);
StackPush(&st, end);
StackPush(&st, begin);
while (!StackEmpty(&st))
{
int left = StackTop(&st);
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int keyi = PartSort1(a, left, right);
if (keyi + 1 < right)
{
StackPush(&st, right);
StackPush(&st, keyi + 1);
}
if (left < keyi - 1)
{
StackPush(&st, keyi - 1);
StackPush(&st, left);
}
}
StackDestroy(&st);
}
快排递归:时间复杂度O(NlogN),空间复杂度O(logN)。
快排非递归:时间复杂度O(NloogN),空间复杂度O(logN)。
7.归并排序
归并排序的思想是分治的思想,要想排好原数组,先将数组分为两半,分别排好两个子序列,之后再将两个数组有序归并。这里依旧使用闭区间。需要一个辅助数组来完成。
代码如下:
void _MergeSort(int* a, int* tmp, int begin, int end)
{
if (begin == end)
return;
int midi = (begin + end) / 2;
_MergeSort(a, tmp, begin, midi);
_MergeSort(a, tmp, midi + 1, end);
int begin1 = begin, end1 = midi;
int begin2 = midi + 1, end2 = end;
int i = begin1;
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 + begin, tmp + begin, sizeof(int) * (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, tmp, 0, n - 1);
free(tmp);
}
归并非递归:
我们在了解了上述归并递归代码后,可以发现,到最后处理的就是两个相邻元素之间的比较归并,所以我们可以使用循环的方法来反方向排序归并,但是在这个过程中需要注意区间的非法访问问题。需要用gap变量来表示反向递归阶段
代码如下:
1.完成一部分,拷贝一部分。
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)
{
int j = 0;
for (int i = 0; i < n; i += gap * 2)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + gap * 2 - 1;
if (end1 >= n || begin2 >= n)
{
break;
}
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);
}
2.全部i拷贝
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)
{
int j = 0;
for (int i = 0; i < n; i += gap * 2)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + gap * 2 - 1;
if (end1 >= n)
{
end1 = n - 1;
end2 = n;
begin2 = end2 + 1;
}
if (begin2 >= n)
{
begin2 = end2 + 1;
}
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, tmp, sizeof(int) * n);
gap *= 2;
}
free(tmp);
}
归并排序递归时间复杂度:O(NlogN),空间复杂度O(N + logN) or logN。
非递归时间复杂度:O(NlogN),空间复杂度:O(N)。
8.计数排序
计数排序的思想很简单,遍历数组,记录数组中元素出现的次数到开好数组对应的下标中然后遍历这个数组进行排列,值得注意的是这是迄今为止我们学到的第一个不用元素间两两比较就可以排序的方法。
适用于数据较集中的场景。
代码如下:
void CountSort(int* a, int n)
{
int min = a[0];
int 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;
int* arr = (int*)malloc(sizeof(int) * range);
if (arr == NULL)
{
perror("malloc fail");
return;
}
memset(arr, 0, sizeof(int) * range);
for (int i = 0; i < n; i++)
{
arr[a[i] - min]++;
}
int k = 0;
for (int i = 0; i < range; i++)
{
while (arr[i]--)
{
a[k] = i + min;
k++;
}
}
free(arr);
}
时间复杂度O(MAX-MIN + I),其中max-min是遍历计数数组的大小,i是某个元素对应出现次数的总和。
空间复杂度O(MAX-MIN)。
排序的稳定性
接下来我们来讨论一下各大排序的稳定性,可能有些人堆排序的稳定性是不正确的,例如:插入排序在面临顺序的时候情况最好,在逆序时情况最差,快排在有序时情况最差等等。
这样的认识是不正确的,稳定性是指就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。若相同则不稳定,反之则稳定。
下面我们一个一个来判断
1.插入排序
很明显,他是稳定的。面对相同元素时停下即可。
2.希尔排序
希尔排序在gap不为1时他的它的变化都会导致不同组的相同元素的相对位置变化。
3.选择排序
在面临5,5,1,1这样的序列下,第一次排序就会将两个5的相对位置发生变化,不稳定。
4.堆排序
建堆要一直变化堆顶的数据,如果面临第一层:8,第二层8,7时第二层的8要与第一层的8必须对换,所以也是不稳定。
5.冒泡排序
虽然有大量的交换,但是他是稳定的,在面对相同元素时不交换即可
6.快速排序
涉及到大量的交换,也不稳定,三条路划分明显不稳定。
7.归并排序
归并排序是可以做到稳定的在面对相等元素时只需要让左面的先进入tmp即可,对上面的代码稍加改造即可。
总结
稳定:冒泡,插入,归并。
不稳定:希尔,堆排,快排,选择。
最后有个问题是,有人可能发现我上面快排后两个动画移动0的时候另
一个0也跟着动,希望有人知道解决的办法,可以告诉我,我是用ppt做
的,用平滑过度ppt。