排序的交换排序与归并排序他来啦,真的不考虑来看看吗?
目录
1 交换排序
1.1 冒泡排序
1.2 快速排序(重点)
1.2.1 挖坑法
1.2.2 前后指针法:
1.2.3 左右指针法
1.2.4 非递归实现快排
2 归并排序
2.1 归并排序的递归实现
2.2 归并排序的非递归实现
3.排序算法复杂度及稳定性分析
基本思想:
- 所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动(升序)。
- 冒泡排序的图解:
- 冒泡排序的特性总结:
- 1. 冒泡排序是一种非常容易理解的排序
- 2. 时间复杂度:O(N^2)
- 3. 空间复杂度:O(1)
- 4. 稳定性:稳定
具体代码:
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void BubbleSort(int* a, int sz)
{
int i = 0;
int j = 0;
int flag=0;
for (i = 0; i < sz - 1; i++)
{
for (j = 0; j < sz - 1 - i; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
flag=1;
}
}
if(0==flag)
{
break;
}
}
}
冒泡排序比较简单,写起来也很容易,这里就不再多说了。
基本思想:
快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列, 左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值 ,然后最左右子序列重复该过程,直到所 有元素都排列在相应位置上为止。将区间按照基准值划分为左右两半部分的常见方式有:a 挖坑法b 前后指针法c 左右指针法(Hoare法)
基本思想:
首先寻找一个坑位(一般选最左边或者中间或者最右边)假设我这里选最左边为坑位,然后用一个关键值key保存坑位值;再从最右边找到小于key的值,找到后就将坑位的值改为找到后的值,然后将找到位置的下标作为新的坑位;再从左边去找到大于坑位的值,找到后同样将坑位的值改为找到后的值,然后将找到位置的下标作为新的坑位;直至相遇。最后将相遇点的值改为key. 这样就完成了将key排到了正确位置。然后分治,用递归将左边与右边分别排,直至所有元素都排到了正确位置。
具体代码实现:
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int begin = left;
int end = right-1;
int pivot = begin;
int key = a[pivot];
while (begin < end)
{
//右边找小 放在左边
while (begin < end && a[end] >= key)
{
end--;
}
a[pivot] = a[end];
pivot = end;
//左边找大 放在右边
while (begin < end && a[begin] <= key)
{
begin++;
}
a[pivot] = a[begin];
pivot = begin;
}
a[pivot] = key;
QuickSort(a, left, pivot - 1);
QuickSort(a, pivot+1, right);
}
注意事项:
- 进行key值单趟排序也可以单独分装一个PartSort函数来实现(通过返回值来确定已经排序好的key值),这里我就不分装了,后面前后指针与左右指针采用的是分装过的。
- 右边找小和左边找大时要加上=,否则可能会造成死循环。
优化版本:
优化版本主要从两点优化:
1 实现快排的3中方法比较高效的原因就是用了二叉树的思想先排出较靠近中间值的元素,但是上面的方法并不能保证我们选择的key是较为靠近中间值,所以我们用一个3数取中的方法来处理。
2 由于排最后的几个元素用快排的效率较低,所以我们用直接插入来排(小区间优化)
具体代码:
//3数取中
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[right] > a[mid])
return mid;
else if (a[right] < a[left])
return left;
else
return right;
}
else
{
if (a[right] > a[left])
return left;
else if (a[right] < a[mid])
return mid;
else
return right;
}
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int index = GetMidIndex(a, left, right);
Swap(&a[left], &a[index]);
int start = left;
int end = right;
int pivot = start;
int key = a[pivot];
while (start < end)
{
while (start < end && a[end] <= key)
{
end--;
}
a[pivot] = a[end];
pivot = end;
while (start < end && a[start] >= key)
{
start++;
}
a[pivot] = a[start];
pivot = start;
}
a[pivot] = key;
//小区间优化
if (pivot - 1 - left < 10)
{
InsertSort(a+left, pivot - 1 - left + 1);
}
else
{
QuickSort(a, left, pivot - 1);
}
if (right - (pivot + 1) < 10)
{
InsertSort(a+pivot+1, right - (pivot + 1) + 1);
}
else
{
QuickSort(a, pivot + 1, right);
}
}
基本思想:
目的都是与挖坑法一样,就是将选出的数排到正确的位置,先保存最左边元素;然后定义最左边下标为prev,下一位为cur,从cur开始找,找到了比保存元素小的就让prev++,然后交换下标为prev与cur元素的值,否则只让cur++,直到cur访问到最后一个元素。最后别忘了将保存的值与下标为prev的元素交换。
具体代码实现:
int PartSort2(int* a, int left, int right)
{
int keyi = left;
int prev = left;
int cur = left + 1;;
while (cur <= right)
{
while (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort2(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort2(a, begin, end);
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
基本思想:
先保存最左边的值,从右边找比保存值小的元素,从左边找到比保存值大的元素,然后交换,直至相遇,再交换相遇点与保存值,将相遇点的下标返回。
具体代码实现:
int PartSort3(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
right--;
}
while (left < right && a[left] <= a[keyi])
{
left++;
}
if (left < right)
{
Swap(&a[left], &a[right]);
}
}
int meeti = left;
Swap(&a[keyi], &a[right]);
return meeti;
}
void QuickSort3(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort2(a, begin, end);
QuickSort3(a, begin, keyi-1);
QuickSort3(a, keyi + 1, end);
}
基本思想:
借助栈来实现(用队列也行,只是栈更能生动的表示分治时不断递归的过程),先让最右边的下标先入栈,再让最左边的下标入栈,当栈不为空时然后出栈,先出的就是最左边的下标,再出最右边的下标,单趟排序后再不断重复入栈出栈过程,直至栈为空就排序好了。
具体代码实现:
void QuickSort3NonR(int* a, int sz)
{
ST st;
StackInit(&st);
StackPush(&st, sz - 1);
StackPush(&st, 0);
while (!StackEmpty(&st))
{
int left = StackTop(&st);
StackPop(&st);
int right= StackTop(&st);
StackPop(&st);
if (left >= right)
{
continue;
}
int keyIndex = PartSort3(a, left, right);
//[left,keyIndex-1] keyIndex [keyIndex+1,right]
if (keyIndex + 1 < right)
{
StackPush(&st, right);
StackPush(&st, keyIndex + 1);
}
if (left < keyIndex)
{
StackPush(&st, keyIndex);
StackPush(&st, left);
}
}
StackDestroy(&st);
}
注意事项:
- 入栈的时候其实先入右和先入左都行,只是去的时候要区分先取的时左还是右。
- 单趟排序用快排的3中方法都行,但是为了调试效果明显最好就不要加3数取中了。
快速排序的特性总结:1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫 快速 排序2. 时间复杂度: O(N*logN)3. 空间复杂度: O(logN)4. 稳定性: 不稳定
基本思想:
归并排序( MERGE-SORT )是建立在归并操作上的一种有效的排序算法 , 该算法是采用 分治 法( Divide and Conquer )的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序 列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有表,称为二 路归并。
图解:
具体代码:
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
{
return;
}
int mid = (left + right) >> 1;
//假设[left,mid] [mid+1,right] 有序
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = left;
//合并
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++];
}
//拷贝过去
for (int i = left; i <= right; i++)
{
a[i] = tmp[i];
}
}
void MergeSort(int* a, int sz)
{
int* tmp = (int*)malloc(sizeof(int) * sz);
_MergeSort(a, 0, sz - 1, tmp);
free(tmp);
}
注意事项:
- index开始并不一定是从0开始,而是从left开始。
- 当数据归并到最后一部分时别忘了将数据拷回去。
基本思想:
归并排序的非递归用栈和队列实现起来比较困难,所以我们用循环来解决。
具体代码:
//分治的非递归
void MergeSortNonR(int* a, int sz)
{
int* tmp = (int*)malloc(sizeof(int) * sz);
if (NULL == tmp)
{
exit(-1);
}
int gap = 1; //每组数组的个数
while(gap= sz)
{
break;
}
//归并过程中右半区间算多了,要修正一下
if (end2 >= sz)
{
end2 = sz - 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++];
}
for (int j = i; j <= end2; j++)
{
a[j] = tmp[j];
}
}
gap *= 2;
}
}
注意事项:
归并过程中右半区间可能就不存在与归并过程中右半区间算多了,要特殊处理。
归并排序的特性总结:1. 归并的缺点在于需要 O(N) 的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。2. 时间复杂度: O(N*logN)3. 空间复杂度: O(N)4. 稳定性: 稳定
排序这方面就到处结束啦,如果有哪儿不对的地方希望各位佬能够指正。