本篇博客是上篇博客【排序算法(一)】的继续。
目录
三、交换排序
1.冒泡排序
该算法也是C语言入门阶段熟知的排序算法,不再做详细论述。
(1)代码实现(升序)
void Swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
void BubbleSort(int* a, int n)
{
for(int i = 0; i < n - 1; i++)
{
bool exchange = false;
for (int j = 0; j < n - i - 1; j++)
{
if (a[j] < a[j + 1]) //
{
Swap(a + j, a + j + 1);
exchange = true;
}
}
if (exchange == false)
break;
}
}
(2)分析
时间复杂度为
空间复杂度为
该算法是稳定的。
2.快速排序
(1)基本思想
任取待排序序列中的某元素作为基准值(一般为第一个元素),通过调整将该基准值放在正确位置上,使得以该基准值为界将序列分成左右两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值(以升序为例);然后左右子序列重复该过程,直到所有元素均排列在正确的位置上。
(2)代码实现(升序)
<1>递归版本
从基本思想中可以看出该算法具有递归性,因此将其实现为递归算法是最自然、最好理解的。
首先列出递归的基本框架:
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)//退出递归的条件
return;
int keyi = PartSort(a, begin, end);//进行调整,将基准值放到正确位置
QuickSort(a, begin, keyi - 1);//左子序列递归
QuickSort(a, keyi + 1, end);//右子序列递归
}
现在的核心就是实现PartSort()函数,将基准值放到正确位置。
如何进行调整呢?其实方法还是有很多的,这里着重介绍3种方法:
①hoare版本
hoare版本是最开始的方法,因为快速排序就是由Hoare于1962年提出的。由于该方法需要注意的细节很多,其他方法都是后来对该方法的优化,写起来不容易出错。
基本想法为:
选定第一个元素为基准值,然后从右边开始逐个往前寻找比基准值小的元素,找到即停,接着从左边开始逐个往后寻找比基准值大的元素,找到即停;交换找到的两个元素;继续上述寻找操作,直到相遇;最后交换相遇位置元素和首元素(基准值)。(可以证明,相遇时的元素一定小于基准值)
//hoare
int PartSort1(int* a, int begin, int end)
{
int keyi = begin;
int left = begin;
int right = end;
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);
return left;
}
②挖坑法
基本思想为:
先将基准值保存起来,并将该位置设为坑位;从右边找到比基准值小的元素后,将该元素值放在坑位里,并更新坑位,将该位置设为新的坑位;从左边找到比基准值大的元素后,将该元素值放在坑位里并更新坑位;重复寻找操作,直到相遇;最后将保存好的基准值放在相遇位置即可。
可以看出,挖坑法与hoare版本的区别在于:挖坑法在单次寻找后就要改变元素的值,而hoare版本在一组寻找(左、右各一次)后才会交换元素的值。
//挖坑法
int PartSort2(int* a, int begin, int end)
{
int key = a[begin];
int holei = begin;
int left = begin;
int right = end;
while (left < right)
{
while (left < right && a[right] >= key) //
{
right--;
}
a[holei] = a[right];
holei = right;
while (left < right && a[left] <= key) //
{
left++;
}
a[holei] = a[left];
holei = left;
}
a[holei] = key;
return holei;
}
③双指针法
基本思想为:
开始时左指针指向首元素(基准值),右指针指向左指针的下一个位置(第二个元素);两个指针的移动规则为:右指针每次向右移动一个元素,左指针只有在右指针指向的元素比基准值小时才会向右移动一个元素,左指针移动后立即交换左右指针指向的元素;直到右指针到达元素末尾停止;(此时左指针指向了最后一个比基准值小的元素)最后交换左指针值和基准值。
//双指针
static int PartSort3(int* a, int begin, int end)
{
int prev = begin;
int cur = begin + 1;
while (cur <= end)
{
if (a[cur] < a[begin]) //
{
prev++;
Swap(a + prev, a + cur);
}
cur++;
}
Swap(a + begin, a + prev);
return prev;
}
至此,一个初始版本的快速排序就写好了。
该版本总是以第一个元素为基准值,虽然方便,但是存在一个弊端:对接近有序的元素集合排序时效率太低,为了解决弊端,可以进一步优化该算法:每次比较首元素、中间元素和末尾元素,选择三数的中间值作为基准值。为了使代码的改动相对较小,可以实现一个挑选函数,在PartSort()函数开始时调用该函数,交换首元素和该函数的返回值即可:
//快排优化:
//三数取中选key值
int ChooseKeyi(int* a, int begin,int end)
{
if (a[begin] > a[end])
{
if (a[(begin + end) / 2] > a[begin])
return begin;
else if (a[(begin + end) / 2] < a[end])
return end;
else
return (begin + end) / 2;
}
else
{
if (a[(begin + end) / 2] < a[begin])
return begin;
else if (a[(begin + end) / 2] > a[end])
return end;
else
return (begin + end) / 2;
}
}
至此,一个相对完美的递归版本的快速排序就完成了,下面对其进行了优化汇总:
int ChooseKeyi(int* a, int begin,int end)
{
if (a[begin] > a[end])
{
if (a[(begin + end) / 2] > a[begin])
return begin;
else if (a[(begin + end) / 2] < a[end])
return end;
else
return (begin + end) / 2;
}
else
{
if (a[(begin + end) / 2] < a[begin])
return begin;
else if (a[(begin + end) / 2] > a[end])
return end;
else
return (begin + end) / 2;
}
}
//hoare
int PartSort1(int* a, int begin, int end)
{
Swap(a + ChooseKeyi(a, begin, end), a + begin);
int keyi = begin;
int left = begin;
int right = end;
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);
return left;
}
//挖坑法
int PartSort2(int* a, int begin, int end)
{
Swap(a + ChooseKeyi(a, begin, end), a + begin);
int key = a[begin];
int holei = begin;
int left = begin;
int right = end;
while (left < right)
{
while (left < right && a[right] >= key) //
{
right--;
}
a[holei] = a[right];
holei = right;
while (left < right && a[left] <= key) //
{
left++;
}
a[holei] = a[left];
holei = left;
}
a[holei] = key;
return holei;
}
//双指针
int PartSort3(int* a, int begin, int end)
{
Swap(a + ChooseKeyi(a, begin, end), a + begin);
int prev = begin;
int cur = begin + 1;
while (cur <= end)
{
if (a[cur] < a[begin]) //
{
prev++;
Swap(a + prev, a + cur);
}
cur++;
}
Swap(a + begin, a + prev);
return prev;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int keyi = PartSort3(a, begin, end);//三个版本选择其一即可
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
<2>非递归版本
所有递归算法均可转化为非递归,该算法也不例外。
只需要借助栈,每次将元素的处理区间进栈,以便左区间处理完后可以找到右区间即可。(该算法用到的有关栈的接口函数与博客【特殊的线性表——栈和队列及其C语言实现】保持一致。)
void QuickSortNonR(int* a, int begin, int end)
{
ST stack;
STInit(&stack);
STPush(&stack, end);
STPush(&stack, begin);
while (!STEmpty(&stack))
{
int left = STTop(&stack);
STPop(&stack);
int right = STTop(&stack);
STPop(&stack);
int keyi = PartSort3(a, left, right);
if (keyi + 1 < right)
{
STPush(&stack, right);
STPush(&stack, keyi+1);
}
if (left < keyi - 1)
{
STPush(&stack, keyi-1);
STPush(&stack, left);
}
}
STDestroy(&stack);
}
(3)分析
若将快速排序的时间以示意图的形式表示,是一颗二叉树,树高为,而每层的时间为O(n):
因此,快速排序的时间复杂度为
递归版本需要不断地开辟栈帧,非递归版本需要额外空间来存放区间信息,栈帧对应的空间可以复用,区间是不断地减半的,因此不论哪种,空间复杂度为
显然,快速排序是不稳定的。
四、归并排序
二路归并排序
(1)基本思想
将已经有序的两个子序列合并,得到一个完全有序的序列。
(2)代码实现(升序)
如何进行合并呢?先开辟一块连续空间用于存放合并的结果,然后利用两个“指针”分别遍历两个有序子序列,遍历时每次比较两个指针所指向值的大小,将值小的元素放在结果区并将该指针移动一个元素;任意一个序列遍历完毕后,另一个序列若有剩余元素,将其逐个添加至结果区即可。
<1>递归版本
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid+1, end, tmp);
int index1 = begin;
int index2 = mid + 1;
int i = begin;
while (index1 <= mid && index2 <= end)
{
if (a[index1] < a[index2]) //
{
tmp[i++] = a[index1++];
}
else
{
tmp[i++] = a[index2++];
}
}
while (index1 <= mid)
{
tmp[i++] = a[index1++];
}
while (index2 <= end)
{
tmp[i++] = a[index2++];
}
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc error");
return;
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
<2>非递归版本
非递归直接模拟合并过程即可。
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc error");
return;
}
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 || begin2 >= n)
break;
if (end2 >= n)
end2 = n - 1;
int j = i;
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);
}
(3)分析
时间复杂度为
归并过程需要开辟空间来暂存归并结果,因此空间复杂度为
归并后相同元素的相对次序并不会改变,二路归并是稳定的。