排序是计算机领域最基础且重要的算法,将一组数据按降序或升序重新排列,如网购网站价格排序等等。
广泛用于==数据库查询,数据分析,搜索算法,==等等。
void BubbleSort(int* a, int n)
{
for (int i = 0;i < n;i++)
{
for (int j = 0;j < n - 1 - i;j++)
{
if (a[j] > a[j+1])
{
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
}
两层for循环遍历,
内层是比较相邻两个数大小,大数放后面,经过内层一次遍历,这组数据最大的数就在末尾了。
再来一次,第二大的数也被排好了。
所以随着外面那层for循环结束,整个数组也被排好了。
因为最大数一次次被交换到末尾,就像一个泡泡上浮的过程,所以叫冒泡排序
内层一次比较之后,最大数就在最后面了,所以下次两两相比就不用找最大的数比较了。
时间复杂度O(n^2)(不知道概念可以看我这篇文章数据结构时间复杂度和空间复杂度分析)
遍历两遍数组明显时间复杂度 n^2,并且就算数组本来有序如(1,2,4,3,5,6,7),
仍旧需要两两相比,所以最好,平均,最坏时间复杂度相同。
void BubbleSort(int* a, int n)
{
for (int i = 0;i < n;i++)
{
int record = false;
for (int j = 0;j < n - 1 - i;j++)
{
if (a[j] > a[j+1])
{
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
record=true;
}
}
if (!record)
break;
}
}
优化,如果排着排着已经有序了,就不需要继续循环两两比对了,比如排着排着变成(1,2,3,4,5,6)那么record还是false,直接退出就行,可以省点时间开销。
void InsertSort(int*a,int n)
{
for (int i = 1;i < n;i++)
{
int end = i - 1;
int tmp = a[i];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
直接插入排序,新来的数插进数组中,就相当于打扑克一张张摸牌的过程。
直接插入排序将一个数组分为已排序和未排序两种状态,新来的牌就相当于从未排序之中拿出来进入有序状态,也就是遍历数组中的数。
过程:
摸了一张牌,再摸一张牌,比较两张牌大小,排好序,
再来第三张,先和第二张比,如果大的话,再和第一张比。
再来第四张,重复此过程,
等摸完牌,手上的牌就是有序的。
——————————
用代码实现,end表示最后一张牌,tmp表示新来的牌,
时间复杂度 : 平均O(n^2),最好O(n), 最好的时候是数组本来就有序,不用进入循环,所以直接遍历一遍数组就行。
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap /= 2;
for (int i = 0;i < n - gap;i += gap)
{
int end = i;
int tmp = a[i + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[gap + end] = tmp;
}
}
}
希尔排序是直接插入的优化,优化直插的这个人叫希尔,直接插入是两两比对,
希尔先是将一个数组分几个组排序,就是跨几个数大致排一下,减少逆序对,呈现大致有序的状态,再两两比较,因为大致有序所以基本比个几次就完全有序了。
直接插入是一张张牌边摸边排,
而希尔排序就是你上厕所去了,旁边的人帮你摸牌扣着,到最后你一把抓起来开始排,先大致排一下,最后两两比对排。
这两种排序在数据小的时候,比如打牌时间复杂度差不多,所以边摸边排和一把抓起来排花的时间一样。
但是一旦数据量上去了,希尔要远胜于直插,比如要比10万个数,未排序的数要依次向前比,导致数据越多,花费时间越多,
而希尔将数组分几个组先比个大概,大的数都在后面,小的都在前面,到最后微微排几次就行,
公认的gap是不断的n/2,
如果100个数gap先等于50,第1,51个数一组,第2,52一组等等,
比完之后gap25 ,第1,26,51,76一组,
重复此过程到最后gap是1进行两两比对。
时间复杂度: 平均O(n^1.3) 明显比直接插入要好,最好O(n^ logn),最坏O(n^2)。
void SelectSort(int* a, int n)
{
int left = 0, right = n - 1;
while (left < right)
{
int maxi = left, mini = left;
for (int i = left + 1;i <= right;i++)
{
if (a[i] > a[maxi])
maxi = i;
if (a[i] < a[mini])
mini = i;
}
swap(&a[mini], &a[left]);
if (maxi == left)
maxi = mini;
swap(&a[maxi], &a[right]);
left++;
right--;
}
}
选择排序,遍历一遍数组,选出最大和最小的数,分别放在两边,
不用考虑已排好的数,缩小数组范围。在遍历一遍选出最大最小数排在两边。
易错点:如果最大的数在最左边,如果不做处理,先最小的数来的这,最左边的数替换掉了,
再进行最大数和最右交换,就会把最小的数放在最右边,
所以要处理,判断一下是这种情况,最小的数与最左交换完之后,就把maxi换为最小的数的坐标。
时间复杂度:O(n^2),明显遍历两遍数组,最好最坏时间都相同。
void Adjustdown(int* a, int parent, int n)
{
//大堆
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
child += 1;
}
if (a[child] > a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
void HeapSort(int* a, int n)
{
//建堆
for (int i = (n - 1 - 1) / 2;i >= 0;i--)
Adjustdown(a, i, n);
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]);
Adjustdown(a, 0, end);
end--;
}
}
堆排序,不了解堆的看我这篇文章数据结构之堆知识,我们先构建一个大堆,构建只需要从最后一个非叶子节点开始向下调整,从后向前遍历。
构建好了之后,将堆顶和最后一个元素交换,最后一个就是最大的数,再缩小堆范围并且将堆顶向下调整(向下调整上面那篇文章有讲),那么堆顶就又是剩下数之中最大的了,再放在堆末尾。
重复此过程,从后向前遍历一遍排序就完成了。
时间复杂度:(On^logn),最好最坏都相同,
int getmidi(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[mid] > a[left])
{
if (a[left] > a[right])
return left;
else if (a[right] > a[mid])
return mid;
else right;
}
else
{
if (a[mid] < a[right])
return mid;
else if (a[left] < a[right])
return left;
return right;
}
}
void QuickSort1(int* a, int left, int right)
{
if (left >= right)
return;
int midi = getmidi(a, left,right);
if (midi != left)
swap(&a[midi], &a[left]);
int begin = left;
int end = right;
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[left], &a[keyi]);
keyi = left;
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi + 1, end);
}
我直接写了三数取中优化版本。
快排,快速排序,顾名思义它很快,很强,
先选择一个基准值,一般选择数组种第一个数,然后从两边遍历数组,右边找到小于基准值的就停,
左边找到大于基准值,然后交换两数,目的就是把大于基准值的全甩到右边,小于基准值的都放左边。
先从右边想左开始遍历,所以最后相遇的数一定是小于基准值的,然后交换基准值和当下的数,左边小于基准值,右边大于基准值,所以基准值排好了,一次会将一个数放在正确位置,在将基准值左半部分和右半部分分别排序(用下标表示),递归结束排序就完成了。
时间复杂度:最好和平均都是O(n^logn),若是没有三数取中,最坏O(n ^2),因为数组可能原来就有序,右边每次都要遍历很长数组,但是优化了三数取中,就是将数组开头末尾和中间取中间值,明显减少时间复杂度。
三数取中函数已给出。
void QuickSort2(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int hole = left;
int key = a[left];
while (left < right)
{
while (left<right && a[right] > key)right--;
a[hole] = a[right];
hole = right;
while (left < right && a[left] < key)
left++;
a[hole] = a[left];
hole = left;
}
a[hole] = key;
QuickSort2(a, begin, hole - 1);
QuickSort2(a, hole + 1, end);
}
void QuickSort2(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int hole = left;
int key = a[left];
while (left < right)
{
while (left<right && a[right] > key)right--;
a[hole] = a[right];
hole = right;
while (left < right && a[left] < key)
left++;
a[hole] = a[left];
hole = left;
}
a[hole] = key;
QuickSort2(a, begin, hole - 1);
QuickSort2(a, hole + 1, end);
}
快排挖洞法
也是找到基准值,但找个变量把基准值存起来,在基准值挖个坑相当于把基准值踢出来,是个空位置,
和上面一样右边找小数,但是找到小的数就把该数填到原来的坑洞里,同时把原来小数的位置挖个新洞,左边找大数也这样做,
到最后把key值填到洞中,也可以形成key左边小与key,右边大于key。
与上述相同的递归。
时间复杂度相同。
void QuickSort3(int* a, int left, int right)
{
if (left >= right)
return;
int midi = getmidi(a, left, right);
if (midi != left);
swap(&a[midi], &a[left]);
int prev = left;
int cnt = left + 1;
while (cnt <= right)
{
if (a[cnt] < a[left] && ++prev != cnt)
{
swap(&a[cnt], &a[prev]);
}
cnt++;
}
swap(&a[prev], &a[left]);
QuickSort3(a, left, prev - 1);
QuickSort3(a, prev + 1, right);
}
快排前后指针法,
后指针一直指向第一个大于基准值的前面的小于基准值的数,前面的指针前进,如果碰到小于基准值的数,让后指针向前一步指向大于基准值的数,两者交换,小数又跑前面,大数又跑后面,
后面就又和上面一样了,
时间复杂度一样
三种排序方法思想都一样的,实现方式略有不同而已
void STInit(ST* ps)
{
assert(ps);
ps->a = (STDataType*)malloc(sizeof(STDataType) * 4);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->capacity = 4;
ps->top = 0; // top是栈顶元素的下一个位置
//ps->top = -1; // top是栈顶元素位置
}
void STDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
STDataType* tmp = (STDataType*)realloc(ps->a,
sizeof(STDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->top] = x;
ps->top++;
}
void STPop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));
ps->top--;
}
void QuickSort4(int* a,int left,int right)
{
ST st;
STInit(&st);
STPush(&st, right);
STPush(&st, left);
while (!STEmpty(&st))
{
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
int keyi=QuickSort3(a, begin, end);
if (keyi + 1 < end)
{
STPush(&st,end);
STPush(&st,keyi+1);
}
if (keyi - 1 > begin)
{
STPush(&st, keyi - 1);
STPush(&st, begin);
}
}
STDestroy(&st);
}
因为要排几十亿个数的话内存不够用,所以可用非递归实现排序
快排非递归实现,用栈辅助,存储每次排列所需的两端点下标,类似二叉树的前序遍历。
因为栈先入后出特性,我们先存右端点再存左,如果栈不为空一直循环,取出栈顶再消除,这里快排3与上面略有不同,只需要返回基准值下标,不需要递归了,所需端点下标就用栈存储,再把分割的小区间所需要的端点存进去。记得销毁开出来的栈。
时间复杂度一样。
void MergeSort(int* a, int left, int right,int* tmp)
{
if (left >= right)
return;
if (right - left + 1 < 10)
{
InsertSort(a+left, right - left+1);
return;
}
int mid = (left + right) / 2;
MergeSort(a,left, mid,tmp);
MergeSort(a,mid+1,right,tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int j = begin1;
while (begin1 <= end1 && begin2<=end2)
{
if (a[begin1] <= a[begin2])
tmp[j++] = a[begin1++];
else
tmp[j++] = a[begin2++];
}
while (begin1 <= end1)tmp[j++] = a[begin1++];
while (begin2 <= end2)tmp[j++]=a[begin2++];
memcpy(a + left, tmp + left, sizeof(int) * (end2 - left + 1));
}
归并排序,分治思想,将数组一分为二,每份都是有序状态,再将两份数组合并起来,类似二叉树后序排列,
先排左边,再排右边,再合并。
用新数组将合并起来的数组存起来,再复制给a,
优化:如果数据很多,成百上千万那种,可以将小范围数组用直接插入排序,要不然最后几层多出很多很多子树和叶子节点。
时间复杂度:最好最坏都是O(nlogn);
void MergeNorR(int* a,int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
for (int i = 0;i < n;i += gap*2)
{
int begin1 = i, end1 = i + gap-1;
int begin2 = i + gap, end2 = i + gap * 2 - 1;
int j = i;
if (begin2 >= n)
break;
if (end2 >= n)
end2 = n - 1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
tmp[j++] = a[begin1++];
else
tmp[j++] = a[begin2++];
}
while (begin1 <= end1)tmp[j++] = a[begin1++];
while (begin2 <= end2)tmp[j++] = a[begin2++];
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
free(tmp);
}
数据几十亿,内存装不下了,就要用归并非递归模式,
用gap表示数组大小,遍历数组,先1,1数组合并,再2,2数组合并,再4,4合并,
记得要调整,如果第二个数组begin2超过数组大小,直接退出,因为无法合并,如果end2超了,那么说不定还有一部分没超范围的,要让这部分和第一个数组合并。
其他和前面相同
void CountSort(int*a,int n)
{
int max=a[0], min = a[0];
for (int i = 0;i < n;i++)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
int* countA = (int*)malloc(sizeof(int) * (max-min+1));
memset(countA, 0, sizeof(int) * (max - min + 1));
for (int i = 0;i < n;i++)
{
countA[a[i] - min]++;
}
int j = 0;
for (int i = 0;i < max - min + 1;i++)
{
while (countA[i]--)
{
a[j++] = i + min;
}
}
free(countA);
}
计数排序,计算每个数据出现的次数,再将根据出现次数存回数组,
如:0, 4,5,1,2,5,5,6,7,2,9
我们需要开9-0+1=10大小数组,
其中countA[5]=3,
countA[0]=1;
countA[3]=0;
再用循环加回去;
缺点很明显只能处理集中数据,一旦出现,3,8888,444,2,999999,
就要开很大内存,造成空间浪费
但是如果,101,105,106,108,101,104,111;
这种可以找出最小值,离散数据,用countA[0]代表101;
优点时间复杂度:O(N+K) 其中k是开辟的数组大小,K 较小时(如排序 0~100 的考试成绩):
计数排序效率极高,接近 O(N),快于任何比较排序。
稳定性指原本数组中数据的相对位置,如1,1,5,4,5,2,1
如果排完后第一个1,第二个1,第一个5相对第二个5没换位置就是稳定,否则不稳定,
冒泡可以控制相邻两者相等就不用交换,
直接插入可以让第二来的停下插在第一个来的后面。
归并可以控制左边数据等于右边,让新数组存左边数据,(因为本来数组就是从左到右的)
计数排序也可以稳定,但是要从后向前填充数组,(我的是从前向后的),
其他要找基准值,而且将数据甩过来甩过去的,就导致交换乱了。不稳定。
如何选择合适的排序算法?
小数据量(N ≤ 100) 插入排序 常数因子小,稳定
大数据量(N > 1000) 快速排序 平均 O(N log N),缓存友好
需要稳定排序 归并排序 稳定且 O(N log N)
内存受限 堆排序 O(1) 空间
整数数据,范围小 计数排序 O(N + K) 极快
数据基本有序 插入排序 接近 O(N)