目录
假设有一个n个数的乱序数组,排升序为例.
1、冒泡排序
冒泡排序就是从第一个元素开始,将相邻的元素逐个比较,一趟下来就可以将一个元素排到正确的位置,因为是排升序,所以第一趟就是将最大的元素排到正确的位置,然后再进行第二趟,总共需要进行n-1趟,因为将n-1个元素排到正确的位置后,剩下的一个元素也在正确的位置
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int flag = 0;
for (int j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
flag = 1;
}
}
if (flag == 0)
break;
}
}
外层循环是要比较的趟数,一个数组有n个元素,则需要比较n-1趟,因为当n-1趟后这n-1个数都在正确的位置了,剩下一个数肯定也在正确的位置
内层循环是控制每一趟要比较几个数,若数组有n个元素,则每一趟要比较n-1,n-2,n-3,...,1
会发现实际上并不需要真的到n-1趟,所以用一个flag来记录,如果一趟下来一个数都没交换,说明已经有序,直接出来即可
冒泡排序的时间复杂度
2、直接插入排序
插入排序就是让i从下标为1的地方开始找(因为默认一个数是有序的),让i指向的这个数与他前一个比较,如果无序(即i指向的数更小),用tmp保存i指向的这个数,然后让j从i-1开始,将数组中每个数都往后挪一位,直到找到a[j]小于tmp的,找到时,让a[j+1]的值赋为tmp即可
void InsertSort(int* a, int n)
{
int i = 0, j = 0;
for (i = 1; i < n; i++)
{
if (a[i] < a[i - 1])
{
int tmp = a[i];
for (j = i - 1; j >= 0 && a[j] > tmp; j--)
{
a[j + 1] = a[j];
}
a[j + 1] = tmp;
}
}
}
直接插入排序的时间复杂度
最坏的情况:当数组逆序时,每一趟都要插入到最前面,时间复杂度是O(N^2)
最好的情况:当数组有序或接近有序时,时间复杂度是O(N)
所以会发现,直接插入排序在不同情况下时间复杂度是不同的,当数组接近有序时,时间复杂度的提升很大,那么可不可以先将数组排成接近有序,然后再对数组进行直接插入排序呢?
3、希尔排序
希尔排序就是再直接插入排序的基础上,先将数组进行预排序,将数组排成接近有序后,再对数组进行直接插入排序。
其做法是,首先确定一个整数gap,将数组分成gap个组,所有距离为gap的数分在一个组内,并对每一组内的数进行直接插入排序,一轮完成以后,缩小gap,直到gap缩小到1,这时候就是直接插入排序。
void ShellSort(int* a, int n)
{
int gap = n;
int i = 0, j = 0;
while (gap > 1)
{
gap = gap / 3 + 1;
for (i = gap; i < n; i++)
{
if (a[i] < a[i - gap])
{
int tmp = a[i];
for (j = i - gap; j >= 0 && a[j] > tmp; j-=gap)
{
a[j + gap] = a[j];
}
a[j + gap] = tmp;
}
}
}
}
这里面gap=gap/3+1是比较好的,其中的+1是为了确保gap能够等于1
在gap=gap/3+1的前提下,希尔排序的时间复杂度大约是O(N^1.3)
4、选择排序
选择排序就是遍历数组,选出其中最小的和最大的元素的下标,并与数组首元素和末尾元素交换,然后缩小范围,选出次小和次大的元素的下标,与数组的第二个元素和倒数第二个元素交换
void SelectSort(int* a, int n)
{
int left = 0, right = n - 1;
while (left < right)
{
int mini = left, maxi = left;
for (int i = left + 1; i <= right; i++)
{
if (a[i] < a[mini])
mini = i;
if (a[i] > a[maxi])
maxi = i;
}
Swap(&a[mini], &a[left]);
Swap(&a[maxi], &a[right]);
left++;
right--;
}
}
此时为什么会出现这样子的情况呢?
所以需要判断一下maxi和left是否相等,若相等,交换了一次之后,maxi原来的值已经到了mini的位置了,需要将mini赋值给maxi
void SelectSort(int* a, int n)
{
int left = 0, right = n - 1;
while (left < right)
{
int mini = left, maxi = left;
for (int i = left + 1; i <= right; i++)
{
if (a[i] < a[mini])
mini = i;
if (a[i] > a[maxi])
maxi = i;
}
Swap(&a[mini], &a[left]);
if (maxi == left)
maxi = mini;
Swap(&a[maxi], &a[right]);
left++;
right--;
}
}
选择排序时间复杂度是O(N^2)
5、堆排序
利用大堆的堆顶元素是整棵树最大的,小堆的堆顶元素是整棵树最小的,可以用堆来进行排序.
以建小堆为例,只需要每次都进行建堆,得到堆顶元素,然后将堆顶元素与堆的最末尾的元素进行交换,然后将堆的大小减小1,这样子堆中最小的元素就到了数组的最后面,因为此时堆的大小减小了1,所以最小的元素不会再被挪动,已经到了正确的位置,然后再依次选次小的,直到堆只剩下一个元素。
排升序,建大堆;排降序,建小堆。
//前提:左右子树是小堆,因为现在实现的是小堆,如果实现的是大堆,那前提就是大堆
void AdjustDown(HPDataType* a, int n, int root)//向下调整算法的实现
{
//找出左右孩子中小的哪一个
int parent = root;
int child = parent * 2 + 1;//直接默认左孩子小,然后再比较,这样子写会比直接定义一个LeftChild,一个RightChild更好
while (child < n)//注意此时有一种极端情况可能会导致越界,即此时的结点有左孩子,但没有右孩子,所以第一个if还要加一个判断条件
{
//找出左右孩子中小的哪一个
if (child + 1 < n && a[child + 1] < a[child])//此时若child+1>=n,也就是说这个结点只有左孩子,直接拿左孩子与父亲比较就可以了,若比较了child+1
//则会造成数组越界
{
child++;
}
//如果孩子小于父亲就交换
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;//这个时候的child是左孩子
}
else//如果孩子都大于父亲,那这个时候就是小堆,直接结束(此时是调到中间就结束了)
{
break;
}
}
}
void HeapSort(HPDataType* a, int n)
{
//1、建堆
//for(int i = n-1;i>=0;i--);这个是把堆中每个数都用一次向下调整算法,但注意,这个的时间复杂度不是n*logn,而是O(n)
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
//2、将第一个数据与最后面的数据交换
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
//继续向下选次小的
AdjustDown(a, end, 0);
end--;
}
}
堆排序的时间复杂度
建堆的时间复杂度是O(N),
一共有N-1次交换,每次交换会从堆顶开始一次向下调整,所以时间复杂度是O(N*logN)
所以堆排序的时间复杂度是O(N*logN)
6、快速排序
6.1 Hoare版本
任取待排序数组中的某一个元素为基准值(一般选取数组第一个元素,这里以选取第一个元素为基准值为例),让right先从右边开始走,找到一个比基准值大的元素,停下,再让left从左边开始走,找到一个比基准值小的元素,停下,让left和right所指向的值交换,再重复上诉步骤,直到left与right相等,然后就将相遇位置的数与数组第一个元素交换(为什么相遇位置一定比基准值小?),结束后,基准值就到了它应该在的位置,再递归基准值的左子数组和右子数组,直到数组都只剩下一个元素,排序完成。
误区一
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int key = a[left];
while (begin < end)
{
if (a[end] > a[keyi])
end--;
if (a[begin] < a[keyi])
begin++;
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], key);
QuickSort(a, left, begin - 1);
QuickSort(a, begin + 1, right);
}
此时是将数组首元素的值赋值给了key,最后并没有真正的交换相遇的元素和第一个元素
修改后
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int keyi = left;
while (begin < end)
{
if ( a[end] > a[keyi])
end--;
if ( a[begin] < a[keyi])
begin++;
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[keyi]);
QuickSort(a, left, begin - 1);
QuickSort(a, begin + 1, right);
}
误区二
此时是找比基准值大于或等于的元素,但是倘若左右都有与基准值相等的元素,那么就会进入死循环,因为交换后依然没有改变
改正的方法为,将找基准值改为单纯找大于或小于,若等于,直接跳过,因为往下递归的过程中依然可以排号
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int keyi = left;
while (begin < end)
{
if ( a[end] >= a[keyi])
end--;
if ( a[begin] <= a[keyi])
begin++;
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[keyi]);
QuickSort(a, left, begin - 1);
QuickSort(a, begin + 1, right);
}
误区三
此时仍然会有错误,当基准值就是整个数组最小的元素的时候,end从右边开始找,会一直减到小于0,造成错误,所以应该要判断一下,当end<=begin时,就没必要再减了,此时也能保证出循环后begin和end在同一个位置
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int keyi = left;
while (begin < end)
{
if (begin < end && a[end] >= a[keyi])
end--;
if (begin < end && a[begin] <= a[keyi])
begin++;
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[keyi]);
QuickSort(a, left, begin - 1);
QuickSort(a, begin + 1, right);
}
为什么相遇的位置一定比基准值小?
注意上面的前提左边做基准值,所以是右边先走
若让右边做基准值,则需要让左边先走,才能保证相遇点比key大
快速排序的时间复杂度是多少?
在前面,我们选取基准值都是选取数组的第一个或最后一个,这个时候没办法判断选取到的基准值是否是数组中最小的,有什么办法可以优化呢?
快速排序的优化一(随机数法)
可以在数组中随机选取一个数,然后把这个数换到数组开头,再让数组开头的数作为key
void QuickSort(int* a, int left, int right)//Hoare法
{
if (left >= right)
return;
int begin = left, end = right;
//随机选key
int randi = rand() % (right - left + 1);
randi += begin;
Swap(&a[left], &a[randi]);
int keyi = left;
while (begin < end)
{
if (begin < end && a[end] >= a[keyi])
end--;
if (begin < end && a[begin] <= a[keyi])
begin++;
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[keyi]);
QuickSort(a, left, begin - 1);
QuickSort(a, begin + 1, right);
}
快速排序的优化二(三数取中法)
可以在a[left],a[right],a[mid]中选取大小在中间的数作为key,其中mid=(left+right)/2
int GetMidi(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
return mid;
else if (a[left] > a[right])
return left;
else
return right;
}
else
{
if (a[mid] > a[right])
return mid;
else if (a[left] < a[right])
return left;
else
return right;
}
}
void QuickSort(int* a, int left, int right)//Hoare法
{
if (left >= right)
return;
int begin = left, end = right;
//三数取中
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;
while (begin < end)
{
if (begin < end && a[end] >= a[keyi])
end--;
if (begin < end && a[begin] <= a[keyi])
begin++;
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[keyi]);
QuickSort(a, left, begin - 1);
QuickSort(a, begin + 1, right);
}
此时若数组有序,直接变成最好
快速排序的优化三(小区间优化)
快速排序中会使用到递归,但是底层的递归有损耗很多的空间,所以当数据较多时,可以往下递归,当递归到数据较少时,没必要往下递归,可以直接使用插入排序来对剩余元素进行排序,以减少递归层数太多带来的损耗
void QuickSort1(int* a, int left, int right)//Hoare法
{
if (left >= right)
return;
//小区间优化:小区间选择走插入,可以减少90%左右的递归
if (right - left + 1 < 10)
{
InsertSort(a + left, right - left + 1);
}
else
{
int begin = left, end = right;
//三数取中
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;
while (begin < end)
{
if (begin < end && a[end] >= a[keyi])
end--;
if (begin < end && a[begin] <= a[keyi])
begin++;
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[keyi]);
QuickSort1(a, left, begin - 1);
QuickSort1(a, begin + 1, right);
}
}
因为快速排序使用了这些优化之后,一定不会出现最坏的情况,基本上都是最好的情况,所以时间复杂度就是O(N*logN)
6.2 挖坑法
挖坑法就是使用三数取中或随机数法选定一个基准值,并将基准值与数组第一个元素交换,将其保存进变量key中,右边先走,找到比key小的就与左边交换,交换完后左边再走,找到比key大的与右边交换,重复此步骤,直到左右指针相遇,将相遇点的值赋值为key
void QuickSort2(int* a, int left, int right)//挖坑法
{
if (left >= right)
return;
if (right - left + 1 < 10)
{
InsertSort(a + left, right - left + 1);
}
else
{
//三数取中
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int key = a[left];
int begin = left, end = right;
while (begin < end)
{
while (begin < end && a[end] >= key)
{
end--;
}
if (begin < end)
a[begin] = a[end];
while (begin < end && a[begin] <= key)
{
begin++;
}
if (begin < end)
a[end] = a[begin];
}
a[begin] = key;
QuickSort2(a, left, begin - 1);
QuickSort2(a, begin + 1, right);
}
}
6.3 前后指针法
前后指针法就是使用三数取中或随机数法选定一个基准值,并将基准值与数组第一个元素交换。初始时让prev指向数组第一个元素,cur指向数组第二个元素。当cur指向的元素大于等于基准值时,++cur,当小于时,++prev,交换prev和cur的值,++cur。直到cur>right,出循环后再将数组首元素与prev指向的值交换
void QuickSort3(int* a, int left, int right)//前后指针法
{
if (left >= right)
return;
if (right - left + 1 < 10)
{
InsertSort(a + left, right - left + 1);
}
else
{
//三数取中
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left, prev = left, cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi])
{
++prev;
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[keyi], &a[prev]);
QuickSort3(a, left, prev - 1);
QuickSort3(a, prev + 1, right);
}
}
6.4 快速排序的非递归
快速排序的非递归需要利用栈后进先出的特点,首先将一组left和right放入栈中,只要栈不为空,就将栈顶的一组left和right取出,将这一组left和right之间的数组利用前面的快速排序方法排序,然后将被排序好的那个位置的左右半边的两组left和right分别放入栈,若左右半边都只有一个数则不用进栈,重复上诉过程,直到栈为空
本质上就是利用栈来保存left和right,类似于递归一样将左右半区往栈中放,再一组一组取出,然后将取出的这一组进行快速排序
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, right);
StackPush(&st, left);
while (!StackEmpty(&st))
{
int begin = StackTop(&st);
StackPop(&st);
int end = StackTop(&st);
StackPop(&st);
//单趟
if (end - begin + 1 < 10)
{
InsertSort(a + begin, end - begin + 1);
}
else
{
//三数取中
int midi = GetMidi(a, begin, end);
Swap(&a[begin], &a[midi]);
int keyi = begin, prev = begin, cur = begin + 1;
while (cur <= end)
{
if (a[cur] < a[keyi])
{
++prev;
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[keyi], &a[prev]);
//[begin,prev-1]prev[prev+1,end]
if (prev + 1 < end)
{
StackPush(&st, end);
StackPush(&st, prev+1);
}
if (begin < prev - 1)
{
StackPush(&st, prev - 1);
StackPush(&st, begin);
}
}
}
StackDestory(&st);
}
7、归并排序
7.1 递归版本
归并排序就是将数组有序的两部分合并在一起的排序。一个数组先将其往下分解,直到都只有一个,因为一个被认为是有序,需要创建一个临时数组,用于临时保存排序好的元素,最后再复制回原数组。当两组中的数都是有序时,定义begin1,end1,begin2,end2分别指向第一组、第二组的开头和结尾,只要begin1<=end1并且begin2<=end2就一直循环,将begin1和begin2指向的数中小的哪一个往tmp数组中尾插,出循环后,再将begin!=end的那一组往tmp中尾插,此时tmp数组中,[begin,end]的区间内的数组就是原先两组合并后的有序数组,结合在一起以后,新的更多元素的有序数组,再将tmp中[begin,end]区间内的数据拷贝回原数组中,就是实现了原数组的部分排序完成,继续往后排,则可完成整个数组的排序。
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin == end)
return;
int mid = (begin + end) / 2;
//[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 = begin;
//依次比较,取小的往tmp尾插
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));
}
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);
}
归并排序时间复杂度的分析
7.2 非递归版本
在快速排序的非递归实现中,我们使用了栈,但是在归并排序的非递归实现中,只使用一个栈是没办法实现的,因为我们在把数组分解时已经把下标从栈中pop了,在要合并时已经找不到下标了,此时使用两个栈是可以的,但是这样过于麻烦,可以直接使用循环。
此时任然开辟一个tmp数组,定义一个gap,gap是1组待归并的数据个数,一次归并两组gap个的数据,gap从1开始,每次gap*2,直到大于n就结束。内层的j表示每一轮第一组的起始位置.
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 += 2 * gap)//j表示每一轮第一组的起始位置,所以是+=2*gap
{
int begin1 = j, end1 = begin1 + gap - 1;
int begin2 = begin1 + gap, end2 = begin2 + gap - 1;
int i = j;
// 依次比较,取小的尾插tmp数组
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) * 2 * gap);
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
此时只能针对于数组元素个数是2的n次方的情况,如果不是,会造成数组越界.
因为begin1是等于j的,所以begin1不可能越界,所以此时只需处理另外三个越界的情况即可。
end1越界:这一小组就不归并了,因为begin1到n-1是有序的
begin2越界:这一小组就不归并了,因为此时begin1到end1是有序的
end2越界:此时仍需要归并,因为[begin1,end1] [begin2,n-1]这两组不一定有序
此时完善了越界问题后,下面的memcpy拷贝一次的字节数也需要修改,因为若这三个其中之一被修改了,那么此时这两组之间的数据个数就不再是2*gap了
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 += 2 * gap)
{
int begin1 = j, end1 = begin1 + gap - 1;
int begin2 = begin1 + gap, end2 = begin2 + gap - 1;
// 越界的问题处理
if (end1 >= n || begin2 >= n)
break;
if (end2 >= n)
end2 = n - 1;
int i = j;
// 依次比较,取小的尾插tmp数组
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;
}
8、计数排序(非比较排序)
计数排序就是开辟一个临时数组统计元素出现的个数,然后再放回原数组的排序方法。
优势:计数排序适用于数据较为集中的数组排序。
局限性:只适用整型的排序
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];
for (int i = 1; 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);//将count的所有值都初始化为0
// 统计次数
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
// 排序
int j = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
a[j++] = i + min;
}
}
}
计数排序的时间复杂度分析
计数排序只需遍历一遍数组,统计元素出现的次数,所有时间复杂度是O(N)
9、各个排序时空复杂度、稳定性比较
直接上结论
稳定性:数组中数值相同的值,排好序后相对位置会不会改变,不改变则稳定,反之亦然。