在之前的两篇博客中,我们分别说了插入排序和选择排序,有兴趣的同学还可以戳链接去看看八大排序算法之选择排序、八大排序算法之插入排序。
交换排序主要说得是冒泡排序和快速排序,思想就和名字一样是用交换来实现的。
1.冒泡排序
基本思想:在要排序的数组中,对当前还没排好序的范围内的全部数,进行依次比较,以升序为例,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
稳定性:因为是冒泡,所以不会改变相同元素的相对位置。
时间复杂度:O(N^2)
冒泡排序比较简单,这里就不画图描述了,直接上代码。
void BubbleSort(int *a, int size)
{
assert(a);
for (int i = 0; i < size; i++)
{
for (int j = 0; j < size - i-1; j++)
{
if (a[j]>a[j + 1])
swap(a[j], a[j + 1]);
}
}
}
2.快速排序
快速排序是我们最常用的排序,这里我们说三种快排的方法:左右指针法、挖坑法、前后指针法。
<1>左右指针法
基本思想:以当前的某个数为基准,然后找出第一个比它大的,第一个比它小的,然后进行交换。两个指针向中间靠近,当左右指针相等的时候,这次循环就结束了。然后不断递归缩小区间,直到所有的区间都有序。
//以一个数为基准,然后找比它大的和小的,然后交换,直到这两个指针相遇
//和中间的那个数进行交换,以这个数为界,左边的都小,右边的都大
int PartSort1(int *a, int left, int right) //左右指针法
{
//优化:三数取中法
//int mid = getMid(a, left, right);
//swap(a[mid], a[right]);
int key = a[right];
int begin = left;
int end = right;
while (begin < end)
{
while (begin < end && a[begin] <= key)
++begin;
while (begin < end && a[end] >= key)
--end;
if (begin < end)
swap(a[begin], a[end]);
}
swap(a[begin], a[right]);
return begin;
}
如果当前数组是个和我们排序相反的有序数组,这么这种情况是快排的最差情况,此时我们可以考虑在选择基准的时候进行优化下。在这里,最简单的优化就是三数取中法。我们在left和right找到中间的位置,然后根据这个mid进行比较,最后在按照我们上面的继续执行。
//当数组有序的时候,快排的情况最差,这个时候可以采用三数取中法
int getMid(int *a, int left, int right)
{
int mid = left + (right - left) >> 2;
if (a[left] < a[mid]) //left mid
{
if (a[mid] < a[right]) //left mid right
return mid;
else if (a[left]>a[right]) //right left mid
return left;
else
return right;
}
else //mid left
{
if (a[mid] > a[right])
return mid;
else if (a[left] < a[right])
return left;
else
return right;
}
}
<2>挖坑法
基本思想:以最后一个数为基准,将这个基准保存在key中,从左找大于key的,找到了就和end交换;然后从右找小于key的,找到了就交换;最后把比key大/小的都放在了合适的位置上
一趟排序的过程如上,如果你还是不明白。就直接跳过去看代码就好了。
int PartSort2(int *a, int begin, int end) //挖坑法
{
int key = a[end];
while (begin < end)
{
while (begin < end && a[begin] <= key)
++begin;
a[end] = a[begin];
while (begin < end && a[end] >= key)
--end;
a[begin] = a[end];
}
a[begin] = key;
return begin;
}
<3>前后指针法
基本思想:也是在找比key大的或者小的,但这里它是从一个方向开始找的。用两个指针分别保存大于和小于,然后找到就进行交换。
这里就不画图了,直接用代码来说明思路。
int PartSort3(int *a, int begin, int end) //前后指针法
{
int prev = begin - 1;
int cur = begin;
while (cur < end)
{
if (a[cur] >= a[end])
cur++;
if (cur != prev && a[cur] < a[end])
{
prev++;
swap(a[cur], a[prev]);
cur++;
}
}
swap(a[++prev], a[end]);
return prev;
}
可以看出,上面三种排序的思路都是一样的,先找出一个基准,然后通过查找比基准大的和小的来进行比较,通过比较,完成排序,然后不断缩小区间去排序。
void QuickSort(int *a, int left,int right)
{
assert(a);
if (left >= right)
return;
//int mid = PartSort1(a, left, right);
//int mid = PartSort2(a, left, right);
int mid = PartSort3(a, left, right);
QuickSort(a, left, mid - 1);
QuickSort(a, mid + 1, right);
}
<4>快速排序的非递归实现
有时候进行笔试或者面试的时候,可能要求你写个快排但是不能使用递归。这个时候千万不能慌,一定要想清除快排的根本是什么,然后借助合适的数据结构来完成。
在上面实现的快排中,我们可以看出递归的部分是不断缩小区间的这个范围,假设现在我们是在操作系统内部,那么每次递归的时候就是创建一个新的栈帧的时候,所以可以联想到栈来实现。也就是说,我们可以利用栈来保存每次更改的区间。其他部分的思路还是一样的,想到这里是不是有种豁然开朗的感觉?话不多说,代码奉上。
void QuickSortNonR(int *a, int left, int right)
{
stack<int> s;
s.push(right);
s.push(left);
while (!s.empty())
{
//每次排序的区间
int begin = s.top();
s.pop();
int end = s.top();
s.pop();
int mid = PartSort3(a, begin,end);
//类似递归的部分,将每次的区间进行压栈
if (begin < mid - 1)
{
s.push(mid - 1);
s.push(begin);
}
if (mid + 1 < right)
{
s.push(end);
s.push(mid + 1);
}
}
}
对快速排序来说,两个指针分别查找相较于基准值大或者小的,所以相同元素的相对位置可能会发生改变,即快排是不稳定的。
到现在为止,常见的排序我们已经说了6种,下篇博客将会说最后的两种,并且对所有的排序算法做个总结。