排序分类
个人在写排序时感觉最难的就是边界控制还有排序每个数是怎么走的。还有快速排序由于要用递归所以要传数组的左右下标,其他排序传数组个数n就行,但是快速排序的right=n-1;我当时因为搞不清下标和数组个数关系导致卡了很久
插入排序
直接插入排序
直接插入排序最容易去去理解就是类想你在打扑克牌发牌你进行调整,发一张你就调整一张,从右往左调整,把小的往前插入。
void InsertSort(int* a, int n)
{
//最后一个位置是倒数第二个
for(int i=0;i<n-1;i++)
{
//for (int i = 0; i < n; i++)
int end = i;
int x = a[end + 1];
while (end >= 0)
{
if (a[end] > x)
{
a[end + 1] = a[end];
end--;
//--end;
}
else
{
break;
}
}
a[end + 1] = x;
}
}
因为end到倒数第二个,最后一个数再插入进去自动进行排序,所以一开始i<n-1。总体思路就是把下一个数据交给x去找小,找到小的就去赋值。找小有两种情况:1,找到小的,2:一直找小到首元素。找到位置后将数据给到它的后一位。在数据为顺序有序时达到最优,为o(n).在顺序为逆序时达到最差,为o(n^2),平均时间复杂度为o(n^2)
希尔排序
希尔排序是个十分优秀的排序,它是在直接插入排序的基础上进行优化的,总体在直接插入排序的排序之前加入了预排序,使得越大的数越在右边,越小的数越在左边,数据接近有序,这样可以使得直接插入排序在逆序出现最坏情况得到解决,但是如果数据量太小,也会导致预排序失效。
数据量越大,预排越快,数据量越小,预排越慢。
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end+gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;
}
}
}
gap如果除于2会有点跳的太快了,除3差不多刚刚好,但是除3可能导致gap为0,所以要+1.gap等于1时就是直接插入排序,注意排序的最后一个数据是n-gap,以前的直接插入排序都是+1-1进行操作,希尔就是+gap-gap进行操作。希尔排序时间复杂度平均大概是o(n^1.3).最好是o(n)。
选择排序
选择排序
选择排序可以说是最拉的一个排序了,因为不管何种情况下它时间复杂度都是o(n^2)(因为它不知道是有序的 ).总体思路就是左右定义为最小值和最大值,遍历数组找到最大和最小值,进行交换,然后left++,right--。
void SelectSort(int* a, int n)
{
int left = 0;
int right = n-1;
while (left < right)
{
int maxi = left;
int mini = left;
for (int i = left; i <= right; i++)
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
swap(&a[left], &a[mini]);
if (maxi == left)
{
maxi=mini;
}
swap(&a[right], &a[maxi]);
++left;
--right;
//区间是不断缩小的
}
}
值得注意的是选择排序有一种特殊情况需要特判一下,当最大值一开始在最左边时,由于前面交换了最小值到最左边,所以此时的最大值已经被交换了,所以在最大值不在原来位置的情况下,我们要将最小值位置赋值给最大值位置
堆排序
堆排序核心思想就是建堆,然后进行向下排序,如果排升序就建大堆,排降序就建小堆,这个我在二叉树那一节讲过就不展开讲解了。堆排序建堆时间复杂度是o(n),向下调整时间复杂度是log^n
堆排序时间复杂度是o(n+n*log^n)
void AdjustDown(int* a,int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child+1<n&&a[child] < a[child + 1])//少了个child+1<n就排不对,n是随时变的
{
child++;
}
if (a[child] > a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
assert(a);
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a,n,i);
}
for (int i = n - 1; i >= 0; i--)
{
int end = i;
swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
}
}
在向下调整算法时因为掉了child+1<n而导致排序不对,因为它的n是随时变化的(向下调整的end就是n),向下调整是从倒数第一个位置的非叶子节点开始调整,如果是升序就建大堆,此时最大的数就在堆顶,从最后一个数开始把倒数第一个数与堆顶的数进行交换,这是最大的数就在数尾,再从最后一个数开始向上进行向下调整算法,这样最大的数就在堆顶。
交换排序
冒泡排序
冒泡是典型的时间复杂度为o(n^2)的排序,值得注意的就是越界问题
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n; i++)
{
int change = 1;
for (int j = 1; j < n-i ; j++)
{
if (a[j-1] > a[j ])
{
swap(&a[j-1], &a[j]);
change= 0;
}
}
if (change == 1)
{
break;
}
}
}
总体思路就是把最大值移到最后一个位置,依次循环下去
快速排序
快排是比较难懂的一个排序,它有三种实现方法:指针法,挖坑法,左右指针法。算法大部分时间时间复杂度是o(n*log^n),在数组为有序时达到o(n),在数组是逆序时达到最差是o(n^2),不过这种最差的情况可以通过三数取中来解决,但是当数组都是相同的数时,快排就不再适用了。核心思想就是找到通过排序使得数组找到key值,让key左边的数小于key,让key右边的数大于key。再通过递归使得子区间减小左右有序。
指针法
int partition(int* a,int left,int right)
{
//指针法
int mini=MidIndex(a, left, right);
swap(&a[mini], &a[left]);
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]);
return left;
}
指针法的思路就是左边找大,右边找小于key的值然后交换使得大的数在右边,小的数在左边。因为中间值一开始在最左边,所以交换完成后,跳出循环还要将交点位置的值与最左边位置交换。
指针法在用最左边做key时,一定要先从右边开始去找小
挖坑法
int partition(int* a,int left,int right)
{
//挖坑法
int mini = MidIndex(a, left, right);
swap(&a[mini], &a[left]);
int keyi = a[left];
int pivot = left;//不是keyi
while (left<right)
{
while (left<right&&a[right] >= keyi)
{
--right;
}
a[pivot] = a[right];
pivot = right;
while (left<right&&a[left] <= keyi)
{
++left;
}
a[pivot] = a[left];
pivot = left;
}
a[pivot] = keyi;
return pivot;
}
核心思想,让最左边为坑,从右开始找小于坑的这个数,找到了将找到的这个数赋值到坑里,让找到的这个数为坑,然后循环往复的左右左右找并赋值新坑,当然可能有一直找不到直到超出数组的情况所以特判left<right,最后循环结束,将一开始的最左边保存的值赋值到最后的坑中
前后指针法
int partition(int* a, int left, int right)
{
int prev = left;
int cur = left+1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi])
{
swap(&a[++prev], &a[cur]);
}
cur++;
}
swap(&a[prev], &a[keyi]);
return prev;
}
前后指针法是推荐使用的一种方法,由于没有太多特判条件,写起来也方便不易出错
核心思想就是利用前后指针的思想,一个往前走的指针如果找到比key小的数就交换后面那个指针并使后面那个指针往前走一步
快排递归递归版
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = partition(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
快排就是不断缩小区间使得左右区间有序的
如果当最左边值为最大/小值,进行预排序就会有困难,因为我们要找的是中间值,所以写个找中间值算法就可以使得特殊情况不那么尴尬
int MidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 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[right] < a[mid])
{
return mid;
}
else if (a[right] < a[left])
{
return right;
}
else
{
return left;
}
}
}
快速排序非递归
当快排当面对23232323....这样的情况,递归层次太深,递归版快排就可能过不了了
void QuickNonR(int* a, int left, int right)
{
ST st;
StackInit(&st);
StackPush(&st, left);
StackPush(&st, right);
while (!StackEmpty(&st))
{
int end = StackTop(&st);
StackPop(&st);
int begin = StackTop(&st);
StackPop(&st);
int keyi = partition(a, begin, end);
if (keyi+1<end)
{
StackPush(&st, keyi + 1);
StackPush(&st, end);
}
if (begin < keyi - 1)
{
StackPush(&st, begin);
StackPush(&st, keyi - 1);
}
}
StackDestory(&st);
}
快排非递归利用栈出栈入栈的操作将整个数组不断分割,从而达到和递归差不多的效果。
归并排序
归并排序也是利用递归思想,将整体数据分成一个个数据,再将一个个数据进行比较放入另一个数组,即先使每个子序列有序,再将子序段有序.时间复杂度是o(n*log^n)
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
{
return;
}
int mid = (left + right) / 2;
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
int begin1 = left;
int end1 = mid;
int begin2 = mid + 1;
int end2 = right;
int i = left;
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++];
}
for (int i = left; i <= right; i++)
{
a[i] = tmp[i];
}
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
printf("malloc fail");
exit(-1);
}
_MergeSort(a,0,n-1,tmp);
free(tmp);
tmp = NULL;
}
计数排序
计数排序在数据量集中的时候好用,可以达到时间复杂度为o(n+range),核心思想就是计算每个数出现的次数,再根据它的最大值和最小值将它一一映射到创建的数组中。
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* countArray = (int*)malloc(sizeof(int) * range);
memset(countArray, 0, sizeof(int) * range);
//设想min是1000,数组从1000开始,1000下标为0;
//计算次数
for (int j = 0; j < n;j++)
{
countArray[a[j] - min]++;
}
int j = 0;
for (int k = 0; k < range; k++)
{
while (countArray[k]--)
{
a[j++] = k + min;
}
}
}
稳定性
说一个排序具有稳定性就是说这个排序在数据有相同的数时,在排序过程中相对位置不发生改变
那我们说这个排序具有稳定性
直接插入排序具有稳定性:直接插入排序就像摸扑克牌不会打乱原来的位置
希尔排序不具稳定性:值相同的两个数分到了不同组会打乱顺序
选择排序不具稳定性:如541509中第一个5和0交换就不对
堆排序不具稳定性:由于堆排序从下往上调整的,会打乱顺序
冒泡排序具有稳定性:冒泡只是找到最大值放到最后并不会改变原来位置
快速排序不具有稳定性:指针法把大的放到右边,相同值会改变位置
归并排序具有稳定性:相等时先下左边的
计数排序不具稳定性