一、插入排序
1.1 思想
插入排序思想较为简单,其实现思想为:
要插入第 i 个元素,在前 i - 1个元素已经有序的情况下,此时将第 i 个元素依次与 i - 1、i - 2......2、1元素依次比较,找到其适合进入的位置即可。
可见下面过程图:
下面是插入的流程图:
以上为一趟插入排序,插入一个数的情况。
那么,对整个无序的数组进行插入排序,则就是依次对数组的每个元素都进行插入,找到自己的合适位置。即从数组的第二个元素开始依次插入,直到数组的最后一个元素为止。
1.2 实现
// 插入排序
void InsertSort(int* a, int n)
{
int i = 0;
for (i = 0; i < n; i++)
{
int end = i;
int tmp = a[end];//保存要排序的数
while (end >= 0)
{
if (end - 1 >= 0 && tmp < a[end - 1])
{
a[end] = a[end - 1];
}
else
break;
end--;
}
a[end] = tmp;
}
}
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
二、希尔排序
2.1 思想
希尔排序,简单来说就是多组插入排序。
插入排序,其根本是在步长为1的情况下,对每个数进行插入直到数组的最后一个元素。
而希尔排序,其步长是变化的,但最后会变为1,步长为几,则就有几组,为1,则说明所有元素为1组。确定好步长,则就确定了组数,然后分别对每组进行插入排序,之后再变化步长,再进行插入排序,直到步长为1时进行最后一次插入排序。下面通过画图来了解下分组。
因此,确定好了步长,则就确定好了组数。之后则就分别对每组进行插入排序即可。每组排序完后,变化步长,一般步长的变化都为缩小2倍。直到步长为1即可。步长越小,数据就越有序,这点应该大家比较清楚的。
2.2 实现
// 希尔排序
void ShellSort(int* a, int n)
{
int gap = 5;//首先,确定一个初始步长
while (gap >= 1)
{
gap = gap / 2;//变化步长,最终步长为1
int i = 1;
int j = 0;
for (j = 0; j < gap; j++)//组数(步长为几,组数就为几)
{
for (i = j; i < n; i += gap)//各组的插入排序,也就是将插入排序中的1全变为gap
{
int end = i;
int tmp = a[end];
while (end >= 0)
{
if (end - gap >= 0 && tmp < a[end - gap])
{
a[end] = a[end - gap];
}
else
break;
end -= gap;
}
a[end] = tmp;
}
}
}
}
时间复杂度:O(n^1.25) 到 O(1.6 * n^1.25)
空间复杂度:O(1)
稳定性:不稳定
三、选择排序
3.1 思想
选择排序思想极为简单,即每次从所有元素中选出最大和最小的元素,分别放在元素的起始和终止位置,直至待排序的元素全部排完。
基本流程图如下所示:
其思想较为简单,就是找到最大值和最小值后,交换即可。
但在遇到下面这两种情况时,交换需要注意的一点为:
注意1:
如果先让 left 与 mini 进行交换,那么,在交换时需要注意一种特殊情况(如下):
遇见这种情况时,交换了 left 和 mini 时,此时,最大值移动到了 mini 下标上,但最大值下标还是在 left 位置上,那么需要变化下 maxi 下标了,即让 maxi = mini即可。
注意2:
如果先让 right 与 maxi 进行交换,那么,在交换时需要注意一种特殊情况(如下):
遇见这种情况时,交换了 right 和 maxi 时,此时,最小值移动到了 maxi 下标上,但最小值下标还是在 right 位置上,那么需要变化下 mini 下标了,即让 mini = maxi即可。
3.2 实现
// 选择排序
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
while (left <= right)
{
int i = 0;
int maxi = left;
int mini = left;//最大值和最小值的初始值
for (i = left; i <= right; i++)
{
if (a[maxi] < a[i])
{
maxi = i;
}
if (a[mini] > a[i])
{
mini = i;
}
}
Swap(&a[left], &a[mini]);//先交换的 left 和 mini 则为注意1
if (maxi == left)//注意1
{
maxi = mini;
}
Swap(&a[right], &a[maxi]);
left++;
right--;
}
}
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
四、堆排序
4.1 何为堆?
要想了解堆排序的思想,首先要了解何为堆,堆是二叉树的一种,属于完全二叉树。
其分为两类:大堆:父亲节点均大于其孩子节点,其根节点为其全部元素的最大值 小堆:父亲节点均小于其孩子节点,其根节点为其全部元素的最小值
因此其性质为:堆中某个节点的值总是不大于或不小于其父节点的值。
例如:
ok,了解完什么是堆后,我们就可以来进行了解堆排序了。
4.2 思想
堆排序即为,先对原数组元素建堆(升序建大堆,降序建小堆),之后每次交换根节点与最后一个元素的值,此时最后一个元素一定为其整个数组的最大值或最小值,因此最后一个元素就已经调整好了,之后再调整根节点(向下调整算法),在进行根节点与倒数第二位元素交换................直至所有元素均在合适位置上。
其实现主要依靠于堆的建立,这里给出一种方法:即向下调整建堆:
建堆的时候首先找到倒数第一个非叶子节点,从这一节点开始进行调整。直至调整至数组的第一个元素。
如升序,其代码实现:
// 堆排序
void AdjustDwon(int* a, int n, int root)//向下调整每次都会找到最大的(升序)
{
int parent = root;
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
child++;
}
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void Jiandui(int* a, int n)
{
int i = 0;
//向下调整建大堆(升序)
for (i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDwon(a, n, i);
}
}
建完堆后,完成排序操作即只需要根节点与最后一个元素交换,交换完调整................交换,调整...............。其现在思维构架图暂未作出,等我下次出个专题进行说明!!!!!
4.3 实现
// 堆排序
void AdjustDwon(int* a, int n, int root)//向下调整每次都会找到最大的
{
int parent = root;
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
child++;
}
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
int i = 0;
//向下调整建大堆
for (i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDwon(a, n, i);
}
//堆排序
for (i = n - 1; i >= 0; i--)
{
Swap(&a[0], &a[i]);//交换
AdjustDwon(a, i, 0);//向下调整
}
}
时间复杂度:O(N * log N)
空间复杂度:O(1)
稳定性:稳定
五、冒泡排序
5.1 思想
冒泡排序,老生常谈了。其思想也较为简单。
两两进行比较交换,使其按照从大到小或从小到大顺序进行排列。每次排序后,就会使最大或最小的数排在元素的尾部。
下面为其过程图:
每进行一次冒泡排序,就会使最大的数(升序)排在了最右边,下一次就可以不去与这最大的数比较了。其比较次数也就减小了1。
5.2 实现
// 冒泡排序
void BubbleSort(int* a, int n)
{
int i = 0, j = 0;
for (i = 1; i <= n; i++)
{
for (j = 0; j < n - i; j++)//每次排好一个最大的数,其比较次数就减一
{
if (a[j] > a[j + 1])//升序
{
Swap(&a[j], &a[j + 1]);
}
}
}
}
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
六、快速排序
6.1 思想
快速排序其思想为:任取全部元素中的某个元素作为基准值,利用这一基准值将全部元素分为两部分,一部分元素小于其基准值,另一部分元素大于其基准值。这两部分分别位于基准值的左右两边。最后重复此过程,直至所有元素都排在指定位置上。
6.2 实现
6.2.1 递归方式
6.2.1.1 实现一 hoare版本(初始版本)
hoare版本的实现方法为:
以上,为1次排序结果,一直重复此过程,直至所有值都在指定位置上。
在上述过程中,key下标的值一直为最左边的值,对此,可以进行优化方案对key值进行调整。
优化方案为:数组最左边、最右边和数组中间这三者值取其中间大小的值。即三数取中。
代码:
// 快速排序递归实现
// 快速排序hoare版本
//三数取中
int midthree(int* a,int left, int right)
{
int mid = (left + right) / 2;
if (a[left] > a[right])
{
if (a[mid] > a[left])
{
return left;
}
else if (a[right] > a[mid])
{
return right;
}
else
return mid;
}
else
{
if (a[mid] < a[left])
{
return left;
}
else if (a[right] < a[mid])
{
return right;
}
else
return mid;
}
}
int PartSort1(int* a, int left, int right)
{
int mid = midthree(a, left, right);
Swap(&a[left], &a[mid]);
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[right], &a[keyi]);
return right;
}
6.2.2 实现二 挖坑法
挖坑法的实现方法为:
其基本过程如上图所示。
代码实现:
// 快速排序挖坑法
//三数取中
int midthree(int* a,int left, int right)
{
int mid = (left + right) / 2;
if (a[left] > a[right])
{
if (a[mid] > a[left])
{
return left;
}
else if (a[right] > a[mid])
{
return right;
}
else
return mid;
}
else
{
if (a[mid] < a[left])
{
return left;
}
else if (a[right] < a[mid])
{
return right;
}
else
return mid;
}
}
int PartSort2(int* a, int left, int right)
{
int mid = midthree(a, left, right);
Swap(&a[left], &a[mid]);
int pit = a[left];
int piti = left;
while (left < right)
{
while (left < right && a[right] >= pit)//右边找小的
{
right--;
}
a[piti] = a[right];
piti = right;//新的坑位
while (left < right && a[left] <= pit)//左边找大的
{
left++;
}
a[piti] = a[left];
piti = left;//新的坑位
}
a[left] = pit;//结束后,将key值填入坑位
return left;
}
6.2.3 实现三 前后指针法
前后指针法的实现方法为:
基本思想过程如上图所示:
代码实现:
// 快速排序前后指针法
//三数取中
int midthree(int* a,int left, int right)
{
int mid = (left + right) / 2;
if (a[left] > a[right])
{
if (a[mid] > a[left])
{
return left;
}
else if (a[right] > a[mid])
{
return right;
}
else
return mid;
}
else
{
if (a[mid] < a[left])
{
return left;
}
else if (a[right] < a[mid])
{
return right;
}
else
return mid;
}
}
int PartSort3(int* a, int left, int right)
{
int mid = midthree(a, left, right);
Swap(&a[left], &a[mid]);
int keyi = left;
int pre = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi])//小于key下标值时
{
pre++;//先加加prev
Swap(&a[cur], &a[pre]);//再交换cur和prev
}
cur++;//再加加cur
}
Swap(&a[keyi], &a[pre]);//循环结束后,让此时的prev与key进行交换
return pre;
}
在上述三种方法实现后,该进行递归整体结构的实现了。观察上述三种结构的思想不难发现,其最后结果都使 key 值(6)排在了适合的位置上。
其递归思想如下图:
代码实现(使key排在合适的位置中使用上述三种方式的任意一种都可以):
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int pre = PartSort1(a, left, right);//以pre为分界线,分割成左区间:[left,pre - 1] 右区间:[pre+1,right]
QuickSort(a, left, pre - 1);//递归左区间
QuickSort(a, pre + 1, right);//递归右区间
}
6.2.2 非递归方式
非递归的实现主要是利用 栈 来进行实现,将每次分割形成的两个区间作为栈内元素存入栈内。释放时也是从栈的一端释放。
// 快速排序 非递归实现
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 start = StackTop(&sk);//取出区间左端点
StackPop(&sk);
int pre = PartSort1(a, start, end);//进行分割
//左区间:[start pre - 1] 右区间:[pre + 1 end]
if (pre < end)
{
StackPush(&sk, pre + 1);//区间左端点入栈
StackPush(&sk, end);//区间右端点入栈
}
if (pre > start)
{
StackPush(&sk, start);//区间左端点入栈
StackPush(&sk, pre - 1);//区间右端点入栈
}
}
}
七、归并排序
7.1 思想
归并排序的思想为将已经有序的序列进行合并,得到新有序序列,重复这一过程,直至其所有数据有序。(文字表达的不太清楚,可看流程图)
其流程图为:
上述流程图为可以进行等分的情况,但如果数组个数不可以等分时,对于非递归实现时需要对右边区间做一些调整,具体可看代码。
7.2 实现
对于其实现方式来说,有两种方式。一是递归实现(与二叉树的遍历过程大致相似),二是非递归实现。
首先来看递归实现(递归实现较为简洁,就不做说明了)
void MerSort(int* a,int begin,int end,int* tmp)
{
if (end <= begin)
return;
int mid = (end + begin) / 2;//取中,分割成两个区间 [begin mid] [mid+1 end]
MerSort(a, begin, mid, tmp);//递归左区间
MerSort(a, mid + 1, end, tmp);//递归右区间
//两个无序数组[begin mid] [mid+1 end]合并为有序数组 tmp 内 ----- 基本方法
int end1 = mid, end2 = end;
int begin1 = begin,begin2 = mid + 1;
int index = begin;
while (end1 >= begin1 && end2 >= begin2)
{
if (a[begin1] > a[begin2])
{
tmp[index++] = a[begin2++];
}
else
{
tmp[index++] = a[begin1++];
}
}
while (end1 >= begin1)
{
tmp[index++] = a[begin1++];
}
while (end2 >= begin2)
{
tmp[index++] = a[begin2++];
}
//将有序数组tmp 再拷贝回原数组 a
for (index = begin; index <= end; index++)
{
a[index] = tmp[index];
}
}
// 归并排序递归实现
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
}
MerSort(a, 0, n - 1, tmp);
free(tmp);
}
接下来为非递归实现,注意:非递归实现时,如果数据个数无法等分则需要对区间进行调整。
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
}
int gap = 1;
while (gap < n)
{
int i = 0;
for (i = 0; i < n; i = i + gap * 2)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + gap * 2 - 1;
int index = i;
//2个区间 [begin1 end1][begin2 end2]
//控制好边界条件,如果end1已经超过了n,那么就不需要再进行其他操作,因为原先区间已经是有序的。
//同理,begin2超过了n也是如此,直接退出循环即可。又因为,begin2 > end1,因此,当end1大于n的时候,begin2一定大于n。所以他俩可以归为一种情况,那就是begin2 >= n
//而当end2大于n,begin2未大于n时,任然需要进行归并,但这时必须修正下end2,不修正就越界了,因此,就需要让end2等于数组的最大取值即可。
if (begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
int j = 0;
//初始条件 j不能等于begin1,因为在前面的过程中,begin1已经加加了
for (j = i; j <= end2; j++)
{
a[j] = tmp[j];
}
//memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));
}
gap *= 2;
}
free(tmp);
}
时间复杂度:O(N * log N)
空间复杂度:O(N)
稳定性:稳定
八、计数排序
8.1 思想
计数排序,其思想与名字很相似。
思想:开辟一个新数组,新数组的范围为要排序数组的最大值和最小值之差。新数组的下标表示要排序数组的每个数的数值,每个下标所对应的值表示每个数出现的次数,没有在要排序数组中出现的下标其值为0。之后再将新数组覆盖到原数组即可。
过程图:
其实现有点技巧。
另外,计数排序其应用的场景比较有限,适合在于数据较为集中时使用。其效率较高。
8.2 实现
// 计数排序
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];
int i = 0;
for (i = 0; i < n; i++)
{
if (min > a[i])
min = a[i];
if (max < a[i])
max = a[i];
}
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int) * range);
memset(count, 0, sizeof(int) * 100);//初始化拷贝数组为0
//统计数据个数
for (i = 0; i < n; i++)
{
count[a[i] - min]++;//技巧1
}
//覆盖原数组
int j = 0;
for (i = 0; i < range; i++)
{
while (count[i]--)//技巧2
{
a[j++] = i + min;
}
}
}
初始化时用到了内存函数,详情了解可见下面这篇文章详细说明。
时间复杂度:O(max(N,范围))
空间复杂度:O(范围)
稳定性:稳定