目录
一.插入排序
1.直接插入排序
直接插入排序就和打扑克的时候,我们一张张整理牌的过程。
此时左边是我们手上已经整理好的牌,现在又摸了一张J,需要将J插入进去,再使左边牌有序,
直接插入排序就是这种思路。
来分析分析一趟排序:
arr[end]>5,将arr[end]后移,end--,此时end<0,已经没有再小的数了,即5就是最小数,在arr[end+1]处插入5。
这就是一趟排序的逻辑。
用代码实现:
int end = 0; //有序区间【0-0】
int tmp = arr[end + 1]; //待排序的数
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + 1] = arr[end]; //往后挪
end--;
}
else
break;
}
arr[end+1] = tmp;
}
然后再来想有N个数,就得走N次排序,外层循环就很简单了
void Insertsort(int* arr, int sz) //插入排序
{
for (int i = 0; i < sz - 1; i++) //控制边界,防止越界
{
int end = i; //有序区间
int tmp = arr[end + 1]; //待排序的数
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + 1] = arr[end]; //往后挪
end--;
}
else
break;
}
arr[end+1] = tmp;
}
}
时间复杂度:O(N^2)
空间复杂度: O(1)
稳定性:稳定
2.希尔排序
希尔排序在我看来就是优化版的直接插入排序,相比与直接插入排序,希尔排序多了一个预排序,可以让数组更快的有序。
每间隔为gap的数为一组,分组排序。
第一趟排序
第二趟排序
gap为1时,就是直接插入排序。
void shellsort(int* arr, int sz)
{
int gap = sz;
int i = 0;
while (gap > 1)
{
gap /= 2;
for ( i = 0; i < sz - gap; i++) //注意越界问题
{
int end = i;
int tmp = arr[end + gap]; //待排序的数
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end]; //往后挪
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
}
时间复杂度:O(N^1.3)
空间复杂度: O(1)
稳定性:不稳定
二.选择排序
1.选择排序
每次排序选出最小/最大/最小最大,将最小数往前插,最大数往后插
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void selectsort(int* arr, int sz)
{
int left = 0;
int right = sz - 1;
while (left < right)
{
int maxi = right;
int mini = left;
for (int i = left; i <= right; i++)
{
if (arr[i] > arr[maxi])
{
maxi = i;
}
if (arr[i] < arr[mini])
{
mini = i;
}
}
swap(&arr[left], &arr[mini]);
if (left == maxi)
{
maxi = mini;
}
swap(&arr[right], &arr[maxi]);
left++;
right--;
}
}
时间复杂度:O(N^2)
空间复杂度: O(1)
稳定性:不稳定
2.堆排序
堆排序利用了大堆/小堆的特性,但由于这一组数本身不是大小堆,所以我们要向下/向上调整建堆,使这组数变成一个大/小堆。
void adjustdown(int*arr,int sz,int parent) // 数组,数据个数,父节点
{
int child = parent * 2 + 1;
while (child < sz)
{
if (child + 1 < sz && arr[child] < arr[child + 1])
//child + 1 < sz —— 防止越界
{
child++;
}
if (arr[parent] < arr[child])
{
swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
建完堆后的样子:
然后把根结点的数据(20)和最后一个结点的数据(8)交换,此时堆又变成了乱序,又需要我们重新调整建堆,但这次建堆的范围少了1,因为最大的数我们已经换到了最后,简而言之,已经排序完成了一个数。
这就是一趟排序,所以完整排序就很简单了
void adjustdown(int*arr,int sz,int parent) // 数组,数据个数,父节点
{
int child = parent * 2 + 1;
while (child < sz)
{
if (child + 1 < sz && arr[child] < arr[child + 1]) //child + 1 < sz —— 防止越界
{
child++;
}
if (arr[parent] < arr[child])
{
swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void heapsort(int* arr, int sz)
{
//升序,建大堆
for (int i = (sz - 1 - 1) / 2; i >= 0; i--) // 建堆 从最后一个根节点开始调整
//【因为不满足向上/向下调整的条件】从下至上一个个调整。
{
adjustdown(arr, sz, i); //数组 元素个数 父节点
}
int end = sz - 1;
while (end > 0)
{
//把最大的甩在最后一个叶子结点,变乱序
swap(&arr[0], &arr[end]);
//再向下调整成大堆
adjustdown(arr, end, 0); //end为总个数 会变化
end--;
}
}
时间复杂度:O(N*logN)
三.交换排序
1.冒泡排序
每次相邻两个数进行比较,小的数往前换,大的数往后换,每一轮冒泡完毕后,最大的数会沉到最后。
void BubbleSort(int* arr, int sz)
{
for (int i = 0; i < sz; i++) //N个数,走N次
{
for (int j = 1; j < sz - i; j++) //每一轮确定一个最大数
{
if (arr[j] < arr[j - 1])
{
swap(&arr[j], &arr[j - 1]);
}
}
}
}
2.快速排序(递归)
快速排序:取数中某一个位基准值,按照基准值将数据分为两个区间,左区间小于基准值,右区间大于基准值,一直重复此过程,直至排序完成。
对于对基准值key的选择,可以是选择每个区间的任意一个数,但是选择不同,可能会影响排序的效率。
1.常规选每个区间的第一个数,此方法若是遇见数据有序的情况,效率会非常低,时间复杂度低至O(N^2)。
2.在区间随机选key,此方法对方法1优化了一点,但由于是随机选择,可能也会出现1的情况。
3.三数取中,此方法比较稳定,能每次较平均的划分左右区间,但对于大量重复数据,仍具有不可处理的情况。
为了方便,本文采取方法1来做示例
①.hoare法
该方法的思路:
right从右至左找小于key的数,left 再从左往右找大于key的数,找到后交换left 和right 指向的数。right,left指针再找,直到两个指针相遇。最后再将key与相遇的位置的数交换,一轮排序完成。
此时一轮排序已成,
再次分出左右区间,继续排序。
void Quicksort(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
int begin = left;
int end = right;
int keyi = left;
while (left < right)
{
while (left < right&&arr[right] >= arr[keyi])
{
right--; //找小
}
while (left < right&&arr[left] <= arr[keyi])
{
left++; //找大
}
swap(&arr[left], &arr[right]);
}
swap(&arr[left], &arr[keyi]); //交换基准值和指针相遇位置的数
keyi = left;
Quicksort(arr, begin, keyi - 1); //递归左区间
Quicksort(arr, keyi+1, end); //递归右区间
}
②.挖坑法
思路:
取基准值key,把key的位置当做一个坑,right指针从右往左找小,找到后放入坑,right位置处形成新的坑,left指针再从左往右找大,找到后放入坑,left位置形成新的坑,如此循环,直到left,right相遇,将基准值key放入坑。一轮排序结束。
再递归分左右区间继续排序,
void Quicksort(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
int key = arr[left];
int keyi = left;
int begin = left;
int end = right;
while (left < right)
{
while (left < right&&arr[right] >= key)
{
right--;
}
arr[keyi] = arr[right];
keyi = right;
while (left < right&&arr[left] <= key)
{
left++;
}
arr[keyi] = arr[left];
keyi = left;
}
arr[keyi] = key;
Quicksort(arr, begin, keyi-1);
Quicksort(arr, keyi+1, end);
}
③.前后指针法
思路:
取基准值key,perv指针指向序列开头,cur指针指向prev下一个位置,若cur指针指向的数大于key,cur往后走,若cur指针指向的数小于key,prev往后走,如果prev!=cur,则交换prev和cur的值,cur往后走,反之,cur往后走。直到cur为空,交换perv指针指向的数和key。
void Quicksort(int* arr, int left, int right)
{
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur<=right)
{
if (arr[cur] > arr[keyi])
{
cur++;
}
else
{
prev++;
if (prev != cur)
{
swap(&arr[prev], &arr[cur]);
}
cur++;
}
}
swap(&arr[prev],&arr[keyi]);
keyi = prev;
Quicksort(arr, begin, keyi-1);
Quicksort(arr, keyi+1, end);
}
3.快速排序(非递归)
用栈实现,把需要排序的区间入栈,排完了就出栈。
void QuicksortNonR(int* arr, int left, int right)
{
ST st;
StackInit(&st);
StackInsert(&st, right);
StackInsert(&st, left);
while (!StackEmpty(&st))
{
int begin = StackTop(&st);
StackPop(&st);
int end = StackTop(&st);
StackPop(&st);
int mid = Quicksort(arr, begin, end); //需要改造一下
if (mid+1 < end) //还有至少2个数,继续进,否则结束
{
StackInsert(&st, right);
StackInsert(&st, mid+1);
}
if (begin < mid - 1)
{
StackInsert(&st, mid-1);
StackInsert(&st, begin);
}
}
StackDestroy(&st);
}
四.归并排序
1.归并排序(递归)
思路:
类似于二叉树的后序遍历,先排序子树,再排序根,将左右子树分割成一个不可再分割的子问题,即只有一个数。
void _MergeSort(int* arr, int left, int right, int* tmp)
{
if (left >= right)
{
return;
}
int mid = (left + right) / 2;
_MergeSort(arr, left, mid,tmp); //分别找左右各一个比较
_MergeSort(arr, mid+1,right,tmp);
int begin1 = left, end1 = mid;
int begin2 = mid+1, end2 = right;
int i = left;
while (begin1 <=end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[i++] = arr[begin1++];
}
else
{
tmp[i++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = arr[begin2++];
}
memcpy(arr + left, tmp + left, sizeof(int) * (right - left + 1));
}
void MergeSort(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
_MergeSort(arr, 0, n-1,tmp);
free(tmp);
}
2.归并排序(非递归)
思路:
递归的思想是从最小区间开始排序,我们非递归也是一样,设置一个gap(间隔),先gap为1的排序,再是gap为2.... ,每次为gap*=2,直到gap达到数据总数为止。
void MergeSortNorR(int* arr, int n)
{
int*tmp=(int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2*gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
//越界修正
if (end1 >= n)
{
end1 = n-1;
begin2 = n;
end2 = n - 1;
}
else if (begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
else if (end2 >= n)
{
end2 = n - 1;
}
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[j++] = arr[begin1++];
}
else
{
tmp[j++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = arr[begin2++];
}
}
memcpy(arr, tmp, sizeof(int) * n);
gap *= 2;
}
free(tmp);
}
注:
begin2,end1,end2注意越界问题!越界需修正。
五、计数排序
优化:
arr=[107,106,108,101,109,105,104];
数组中最大的数是109,我们就要开109个空间的count数组吗?
但是我们只用了7个空间,足足浪费了102个空间,所以我们选择一个优化方式,开范围个空间,这里的范围是数组中的最大值减去最小值。
void CountSort(int* arr, int n)
{
int max=arr[0];
int min=arr[0];
for (int i = 0; i < n; i++)
{
if (arr[i] > max)
{
max = arr[i];
}
if (arr[i] < min)
{
min = arr[i];
}
}
//int* tmp = (int*)calloc(max, sizeof(int)); 可能min不是0
int range = max - min + 1; // 开辟了范围的空间
int* tmp = (int*)calloc(range, sizeof(int));
for (int i = 0; i < n; i++)
{
tmp[arr[i]-min]++; //减少开辟空间 and 可以处理负数
}
int j = 0;
for (int i = 0; i < range; i++)
{
while (tmp[i]--)
{
//arr[j++] = i; 没有复原
arr[j++] = i+min; //复原原数
}
}
}