一.交换排序-冒泡排序
思想:n个数进行排序,进行n-1趟排序,每一趟将相邻的两个元素进行两两比较,将最大的元素排到末尾。没拍完一趟,代排元素的个数-1。
void BubbleSort(int *arr,int n)
{
for(int j = 1;j <= n-1; j++)//n个元素排n-1趟,
{
for (int i = 1; i <= n - j; i++)//第j趟进行两两比较,比较n-j次
{
if (arr[i - 1] > arr[i])
{
swap(&arr[i - 1], &arr[i]);
}
}
}
}
时间复杂度 :比较次数+交换次数
比较次数:n-1趟总的比较次数,n-1+n-2+n-3+...+1=(n^2-n)/2
交换次数:最坏情况下,即数组完全逆序,每次比较都会有交换,故交换次数也为(n^2-n)/2
故最坏情况下时间复杂度为:O(n^2-n)=O(N^2)
交换次数:最好情况下,数组为顺序,每一趟的每一次比较都不需要交换,故交换次数为0
因此最好情况下的时间复杂度为:O(n^2-n)/2=O(N^2)
优化后的冒泡排序:添加标志位,在第j趟排序加入标志位,如果标志位变动,则说明仍有元素进行交换。如果标志位不变,说明元素数据没交换,则可提前结束。
void BubbleSort(int *arr,int n)
{
for(int j = 1;j <= n-1; j++)//n个元素排n-1趟,
{
int flag = 0;
for (int i = 1; i <= n - j; i++)//第j趟进行两两比较,比较n-j次
{
if (arr[i - 1] > arr[i])
{
swap(&arr[i - 1], &arr[i]);
flag = 1;
}
}
if (flag == 0)//交换未发生,数组元素有序,提前结束
break;
}
}
最坏情况下的时间复杂度:比较次数:每趟比较次数之和(n^2-n)/2,每次比较都交换,交换次数:(n^2-n)/2。时间复杂度为O(n^2-n)=O(N^2)。
最好情况下的时间复杂度:比较次数n-1趟(仅第一次比较),交换次数,0次交换。时间复杂度为O(n-1)=O(n)。
二.交换排序-快速排序
hoare版本(无优化):选定一个基准值,定义两个左右指针,将该序列分成两个子序列,左子序列的所有值小于=key,右子序列的所有值大于key。
过程:key为当前数组的第一个元素,left是当前数组最左边元素的下标,right是当前数组左右边元素的下标,如果left对应的值小于=key对应的值,则left向右移动,直到找到比key对应元素大=的位置下标,right向左移动,找到比key小的元素下标right。接着交换left,right对应的元素,直到left=right。此时除了left=right的下标对应的元素和key下标对应的元素外,其余数组中左区间的元素都<=key对应的元素,右区间的元素都>=key对应的元素,(相等的不交换,不必要)现在交换left=right的下标对应的元素(此元素一定要小于key对应的值!!!)与key下标对应的元素,则左区间的元素全部小于key对应的值,右区间的元素全部大于key对应的值。
(此元素一定要小于key对应的值!!!),否则就会遇到这种情况:
那如何确保此元素一定要小于key对应的值?
让right遇left,因为left对应的值一定小于key对应的值,这样,当left不动,right先动,left=right对应的元素与key对应的元素交换,一定是小值与key对应的元素交换。
将这段代码
while (arr[left] <= arr[key]&&left<right)
left++;
while (arr[right] >= arr[key]&&left<right)
right--;
换成这段代码
while (arr[right] >= arr[key]&&left<right)
right--;
while (arr[left] <= arr[key]&&left<right)
left++;
递归什么时候结束?
区间left=right or left>right
void QuickSort(int *arr, int left, int right)
{
if (left >= right)//将大问题拆解为小问题,小到无需处理时终止
return;
int key = left;
int begin = left;//要重定义i和j作为指针,防止后续递归时无法正确划分区间,如QuickSort(arr, left , key-1);
int end = right;
while (begin < end)//left>=right就结束了
{
while (arr[end] >= arr[key] && begin < end)
end--;
while (arr[begin] <=arr[key]&& begin < end)
begin++;
swap(&arr[begin], &arr[end]);
}
swap(&arr[key] ,&arr[begin]);//直接在原始数组上操作
//中间的值固定,左右两个子区间继续快排
//[left,key-1] key [key+1,right]
key = begin;
QuickSort(arr, left , key-1);
QuickSort(arr, key+1, right);
}
换个说法就是:如果继续递归递归-left<right,反之递归结束,left>=right。
时间复杂度 :高度×每一层的操作次数(每一层都要遍历元素)
最优情况下时间复杂度:O(n) × O(log₂n) = O(n log₂n)。每次正好能将数组二分
最坏情况下时间复杂度:O(n) × O(n) = O(n²)。划分不均匀,每次key多对应最小的元素
优化版本:
1. 三数取中法选key
使key为数组中居中的元素(大差不差),保证每次都能将数组二分,不让其退化为n^2
思想:将数组第一个元素下标对应的数 与 数组最后一个元素下表对应的数 与 这两个下标/2对应的元素 作比较,找出三个数中位处中间的数,将此数与最左边的元素交换,也就是key对应的数,保证能将数组二分。
int mid(int *arr, int left,int right)
{
int mid = (left + right) / 2;
if (arr[mid] < arr[left])//mid left
{
if (arr[mid] > arr[right])
return mid;
//接下来left right 比较
else if (arr[left] < arr[right])
return left;
else
return right;
}
else {//left mid
if (arr[mid] < arr[right])
return mid;
//接下来left right 比较
if (arr[right] > arr[left])
return right;
else
return left;
}
}
void QuickSort1(int* arr, int left, int right)
{
if (left >= right)
return;
//取居中的元素下标与key交换
int midder = mid(arr, left, right);
swap(&arr[left], &arr[midder]);
int key = left;
int begin = left;
int end = right;
while (begin < end)
{
while (arr[end] >= arr[key] && begin < end)
end--;
while (arr[begin] <= arr[key] && begin < end)
begin++;
swap(&arr[begin], &arr[end]);
}
swap(&arr[key], &arr[begin]);
//[left,key-1] key [key+1,right]
key = begin;
QuickSort(arr, left, key - 1);
QuickSort(arr, key + 1, right);
}
时间复杂度:O(n log₂n)
2.小区间优化
递归开销占比过高,每一层递归都需要保存栈帧、进行参数传递和返回操作。当区间规模很小时(n=10),单次递归的计算量很小,但递归本身的固定开销成为主要耗时,导致 “得不偿失”。
而且还要多次的划分区间,分区操作的准备工作与实际排序的工作量占比失衡。
递归到小的子区间时,可考虑不用递归,用插入排序替代
//直接插入排序
void InsertSort(int* arr, int n)
{
//end要变+[0,end+1]要变
//[0,end]有序,end+1为待排的元素
for (int i = 0; i <= n - 2; i++)
{
int end = i;
int temp = arr[end + 1];//待排序的元素
//找到合适的位置
while (end >= 0)
{
if (temp < arr[end])//下标为end包括之后的元素全部向后移动覆盖
{
arr[end+1] = arr[end];
end--;
}
else {
break;
}
}
//temp>=arr[end]
arr[end + 1] = temp;
}
}
当区间元素个数<10时,采用直接插入排序
void QuickSort2(int* arr, int left, int right)
{
if (left >= right)
return;
//小区间优化
if ((right - left + 1) < 10)
InsertSort(arr, right - left + 1);
else{
//取居中的元素下标与key交换
int midder = mid(arr, left, right);
swap(&arr[left], &arr[midder]);
int key = left;
int begin = left;
int end = right;
while (begin < end)
{
while (arr[end] >= arr[key] && begin < end)
end--;
while (arr[begin] <= arr[key] && begin < end)
begin++;
swap(&arr[begin], &arr[end]);
}
swap(&arr[key], &arr[begin]);
//[left,key-1] key [key+1,right]
key = begin;
QuickSort(arr, left, key - 1);
QuickSort(arr, key + 1, right);
}
}
快速排序-前后指针版本
1.prev指向的值永远<关键字key,
因为:最开始时,prev在最左边,prev根据cur来走
如果cur指向的值>key指向的值,cur++,prev不变(prev仍然是小值)
如果cur指向的值<=key指向的值,prev++,(此时prev指向的仍然是小值),再交换cur与prev指向的值,然后cur++
2.prev和cur之间的值都是比关键字key对应的值大的元素
因为:一旦cur>key,cur就会++,而prev不动
3.prev+1是cur,或者是比关键字key对应的值大的值,这样才能实现左边比key小,右边比key大
因为:prev走的步数是永远超不过cur的,cur每次都++,prev只有在cur<key时才++
除了prev+1=cur的时候,prev++指向的元素都大于关键字key对应的值,因为只有cur指向的值>关
键字key指向的值时,cur与prev才会拉开真正的差距,当所有元素都小于关键字key对应的值时,
prev++,cur++.当有元素大于关键字key对应的值时,prev不动,cur++。prev指向的值时是小值。
//前后指针,单趟
int PartSort2(int* a, int left, int right)
{
//三数取中
int midder = mid(a, left, right);
swap(&a[midder], &a[left]);
int key = left;
int prev = key;
int cur = prev + 1;
while (cur <= right)
{
//1.
/*while (a[cur] > a[key])
{
cur++;
}
while (a[cur] <= a[key])
{
prev++;
swap(&a[prev], &a[cur]);
cur++;
}*/
//2.
/*if (a[cur] <= a[key])
{
prev++;
swap(&a[prev], &a[cur]);
cur++;
}
else {
cur++;
}*/
if (a[cur] <= a[key] && ++prev != cur)//自身与自身不交换,prev已经++
swap(&a[prev], &a[cur]);
cur++;
}
swap(&a[prev], &a[key]);
return prev;
}
void QuickSort3(int* arr, int left, int right)
{
if (left >= right)
return;
int key = PartSort2(arr, left, right);
QuickSort3(arr, left, key - 1);
QuickSort3(arr, key + 1, right);
}
int main()
{
int arr[] = { 6,1 ,2,7,9,3,4,5,10,8 };
QuickSort3(arr, 0, sizeof(arr) / sizeof(int) - 1);
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
printf("%d ", arr[i]);
return 0;
}
快速排序-非递归版本
借助栈来实现(深度,一边处理完了再处理另一边)
取栈顶空间,进行单趟排序,左右子区间入栈
先将区间[left,right]的右区间的值right入栈,再将左区间的值left入栈。那取出的顺序就是left,right。
不能用队列,因为先存入的先出队,就没有类似于递归的那种调用操作,队列类似层序(一层一层走)
如果区间内只有一个值或者没有值,就不入栈,不用排序了
循环每走1次,就相当于递归1次
//快速排序非递归
void QuickSortNo(int* a, int left, int right)
{
Stack st;
STInit(&st);
STPush(&st, right);//先入右再入左
STPush(&st, left);
while (!STEmpty(&st))
{
int begin= STTop(&st);//取出left
STPop(&st);
int end= STTop(&st);
STPop(&st);
int key = PartSort2(a, begin, end);
//右区间入栈
if(key+1 < end)//先入右,相等表示只有一个值,小于表示至少还有两个值
{
STPush(&st, end);//右
STPush(&st, key + 1);
}
//左区间入栈,符合条件才入栈
if (begin < key-1)
{
STPush(&st, key - 1);//右
STPush(&st, begin);
}
}
STDestory(&st);
}
数据结构的栈相当于递归中调用的栈帧(存在于栈中),但是数据结构的栈不会溢出,存在于堆中,堆>>栈,不用考虑栈太深溢出。