排序在我们生活中应用广泛运用,接下来我们看看几种常见的排序——
一、插入排序
1、直接插入排序
插入排序犹如我们平常玩儿扑克理牌一般,总是将摸到的一张放在比前面大后面小的位置。每次挪动一张。
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++) //外层循环 n-1 次
{
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)。若每一次拿到的牌都为倒序,则需要依次挪动1张、2张、3张、4张...........此时的时间复杂度为1+2+3+4+......+n-2+n-1,即为O(N^2)。
- 空间复杂度——因为不需要额外开辟空间,即空间复杂度为O(1)。
- 稳定性——在每次插入新的牌时,可以不改变 原来同大小牌的顺序位置,即直接插入排序是稳定的排序。
2、希尔排序
希尔排序是建立在插入排序之上的一个排序,它对插入排序进行了极大的优化,从而使插入排序的效率得到了质的飞跃——
它先建立了比较的距离——gap,然后让间隔gap的每个数进行比较,从而实现部分有序。当gap==1的时候,即为最后一次排序。
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1; //一般gap的初始值设为3较为合适
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
- 时间复杂度——O(N*logN)
- 空间复杂度——O(1)
- 稳定性——交换的顺序不确定,不稳定
二、选择排序
1、选择排序
交换函数——
void Swap(HeapData* p1, HeapData* p2)
{
HeapData tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void SelectSort1(int* a, int begin, int end)
{
assert(a);
while (begin < end)
{
int min = begin;
for (int i = begin + 1; i <= end; i++)
{
if (a[i] < a[min])
{
Swap(&a[i], &a[min]);
}
}
begin++;
}
}
//优化后
void SelectSort(int* a, int begin, int end)
{
assert(a);
while (begin < end)
{
int min = begin, max = begin;
for (int i = begin + 1; i <= end; i++)
{
if (a[i] < a[min])
{
min = i;
}
if (a[i] > a[max])
{
max = i;
}
}
Swap(&a[min], &a[begin]);
if (begin == max)
{
max = min;
}
Swap(&a[end], &a[max]);
begin++;
end--;
}
}
若一次选择一个效率较低,我们可以优化一次选择最大和最小
- 时间复杂度——O(N^2)
- 空间复杂度——O(1)
- 稳定性——不稳定
2、堆排序
我们在进行堆排序时,如果需要从小到大排序,则需要建大堆,反之则建小堆——建堆方法如下文章有介绍https://blog.csdn.net/sun_0228/article/details/125046365?spm=1001.2014.3001.5502
void AdjustDown(HeapData* a, int size, int parent)
{
int child = 2 * parent + 1;
while (child < size)
{
if (child + 1 < size && a[child] < a[child + 1])
{
child++;
}
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
//时间复杂度O(N*logN)
void HeapSort(HeapData* a,int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
这里采用向下调堆的方式,每一次都将堆最后数据与堆顶数据进行调换,再依次调整。
- 时间复杂度——O(N*logN)
- 空间复杂度——可在原数组基础上进行建堆,即空间复杂度为O(1)。
- 稳定性——不稳定
三、交换排序
1、冒泡排序
这应该是我们最先接触的排序——
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++) //外层循环 n-1 次
{
for (int j = 0 ; j < n - i - 1; j++) //内层循环 n-1 次
{ //即时间复杂度为n^2
if (a[j] > a[j + 1])
{
int tmp = a[j + 1];
a[j + 1] = a[j];
a[j] = tmp;
}
}
}
}
- 时间复杂度——O(N^2)
- 空间复杂度——O(1)
- 稳定性 ——稳定
2、快速排序
int GetMidIndex(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return end;
}
else
{
return begin;
}
}
else // (a[begin] >= a[mid])
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return begin;
}
else
{
return end;
}
}
}
//hoare法
int PartSort1(int* a, int begin, int end)
{
int left = begin;
int right = end;
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
right--;
}
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
keyi = left;
return keyi;
}
//挖坑法
int PartSort2(int* a, int begin, int end)
{
int key = a[begin];
int piti = begin;
while (begin < end)
{
//向右找小,填坑
while (begin < end && a[end] >= key)
{
end--;
}
a[piti] = a[end];
piti = end;
//向左找大,填坑
while (begin < end && a[begin] <= key)
{
begin++;
}
a[piti] = a[begin];
piti = begin;
}
a[piti] = key;
return piti;
}
// 前后指针法
int PartSort3(int* a, int begin, int end)
{
int keyi = begin;
int prev = begin;
int cur = begin + 1;
//三数取中--------若不加此条件,在数组本就是有序的情况下,则会出现栈溢出情况
int mid = MidIdex(a, begin, end);
Swap(&a[keyi], &a[mid]);
while (cur <= end)
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[cur], &a[prev]);
cur++;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
有三个版本,分别为最初的hoare版和挖坑版到最后的前后指针版本,最初版本限定了若取左边的数作为key则必须从右边开始找,而后面版本做出了相对优化。
- 时间复杂度——O(N*logN)
- 空间复杂度——O(logN)~O(N)
- 稳定性——不稳定
四、归并排序
1、归并排序
归并排序采用了分治的思想,从个别到整体。
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
_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, (end - begin + 1)*sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
- 时间复杂度——O(N*logN)
- 空间复杂度——O(N)
- 稳定性——稳定