目录
交换排序算法基本思想是根据序列中两个记录键值的比较结果来对换这两个记录在序列的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。交换排序有冒泡排序和快速排序两种。冒泡排序我们在C语言阶段就学习过了,所以就简单写一下,主要对快速排序进行学习。
冒泡排序
算法思想
假如要排升序,冒泡排序就是从序列中的第一个元素开始,依次对相邻的两个元素进行比较,如果前一个元素大于后一个元素则交换它们的位置。如果前一个元素小于或等于后一个元素,则不交换它们;这一比较和交换的操作一直持续到最后一个还未排好序的元素为止。
当这样的一趟操作完成时,序列中最大的未排序元素就被放置到了所有未排序的元素中最后的位置上,它就像水中的石块一样沉到了水底。而其它较小的元素则被移动到了序列的前面,就像水中的气泡冒到了水面一样。这就是为什么该算法被叫做冒泡排序的原因。
动图演示:
代码实现
//每排一趟就有一个元素到达最终位置,下一趟需要排序的元素数就少一个
void BubbleSort(int* a, int n) //n是元素个数
{
int i = 0;
int j = 0;
for (j = 0; j < n - 1; j++)
{
for (i = 0; i < n - 1 - j; i++) //一趟
{
if (a[i + 1] < a[i])
{
int tmp = a[i];
a[i] = a[i + 1];
a[i+1] = tmp;
}
}
}
}
总结:
1.冒泡排序相比于其他排序,是一种非常容易理解且稳定的排序
2.时间复杂度:O(N^2)
3.空间复杂度:O(1)
快速排序
算法思想
快速排序是Hoare于1962提出的一种二叉树结构的交换排序方法。
思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后对左右子序列重复该过程,直到所有元素都排列在相应的位置上为止。
下面我们一起学习一下递归实现快排的方法吧!
递归实现快速排序
1.hoare版本
取一个数为key,从左右两端开始查找,右先走,右端找比key小的数,左边找比key大的数,找到后交换,然后左右继续找,直至左右相遇就停止,再交换相遇点与key的值,这样一趟查找下来待排序key的左部分均比key小,右部分均比key大。再依次处理它的左右两部分。
依次往下查找,规则类似于二叉树的前序遍历,很简单下面我们用代码实现一下。
hoare版本代码实现
//1
void QuickSort1(int* a, int begin, int end)
{
if (begin >= end) //当[begin,end]间只有一个数据或该区间不存在,不用调整
return;
int key = begin;
int left = begin + 1;
int right = end;
while (left < right)
{
while (left < right && a[right] > a[key]) //加left<right条件,避免右在找小的过程
//中遇到左不停止仍继续找
{
right--;
}
while (left < right && a[left] <= a[key]) //加left<right条件,避免右找到小,
//左遇到右后不停止仍继续找
{
left++;
}
Swap(&a[left], &a[right]);
}
if(a[right] < a[key]) //right未找到小就相遇停止,此时a[right]>a[key],不必交换
Swap(&a[key], &a[right]);
key = left; //更新key值,a[key]已排到对应位置不用再变,调整两侧区间
QuickSort1(a, begin, key - 1); //[begin,key-1]
QuickSort1(a, key + 1, end); //[key+1,end]
}
//2
void QuickSort1(int* a, int begin, int end)
{
if (begin >= end) //当[begin,end]间只有一个数据或该区间不存在,不用调整
return;
int key = begin;
int left = begin;
int right = end;
while (left < right)
{
while (left < right && a[right] > a[key]) //加left<right条件,避免右在找小的过程
//中遇到左不停止仍继续找
{
right--;
}
while (left < right && a[left] <= a[key]) //加left<right条件,避免右找到小,
//左遇到右后不停止仍继续找
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[key], &a[right]);
key = left; //更新key值,a[key]已排到对应位置不用再变,调整两侧区间
QuickSort1(a, begin, key - 1); //[begin,key-1]
QuickSort1(a, key + 1, end); //[key+1,end]
}
但是这种方法对于有序数据很不友好,效率不高,因为采用固定选key,每次以最左边的数为key,R开始找左,一直从最右走到最左,要进行N趟,时间复杂度就成了O(N^2)。我们对选key的方法进行优化,有两种:(1)随机数选key,(2)三数取中法,选不是最大也不是最小的数作key。我们选择使用第二种方法。
当依次往下排序的时候区间会变得很小,这时候再递归排序就比较多余,可以选择插入排序进行优化。在最后三层或四层选用插入排序,递归次数将大大减少。
优化后代码
int GetMid(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] > a[mid])
{
if (a[end] < a[mid]) //begin mid end
return mid;
else if (a[begin] > a[end]) //begin end mid
return end; //end begin mid
return begin;
}
else //begin < mid end
{
if (a[end] > a[begin])
{
if (a[end] < a[mid]) //begin end mid
return end;
else return mid; //begin mid end
}
else return begin; //end begin mid
}
}
void QuickSort1(int* a, int begin, int end)
{
if (end - begin + 1 <= 10) //小区间优化
{
InsertSort(a + begin, end - begin + 1);
}
else
{
if (begin >= end)
return;
int mid = GetMid(a, begin, end); //三数取中
Swap(&a[mid], &a[begin]);
int key = begin;
int left = begin;
int right = end;
while (left < right)
{
while (left < right && a[right] >= a[key])
{
right--;
}
while (left < right && a[left] <= a[key])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[key], &a[right]);
key = left;
QuickSort1(a, begin, key - 1);
QuickSort1(a, key + 1, end);
}
}
//将单趟拎出来
int PartSort1(int* a, int begin, int end)
{
int mid = GetMid(a, begin, end);
Swap(&a[mid], &a[begin]);
int key = begin;
int left = begin;
int right = end;
while (left < right)
{
while (left < right && a[right] >= a[key])
{
right--;
}
while (left < right && a[left] <= a[key])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[key], &a[right]);
return left;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
if (end - begin + 1 <= 10)
{
InsertSort(a + begin, end - begin + 1);
}
else
{
int key = PartSort1(a, begin, end);
QuickSort(a,begin,key - 1);
QuickSort(a, key + 1, end);
}
}
2.挖坑法
挖坑法是对单趟排序的优化。选取一个数作key(一般是最左端或最右端的数),在此处形成一个坑位hole。从左右两端开始走,右端先走,找到比key小的值,将该值放在坑位处,更新hole为右端找到的数的位置,形成新的坑位。接下来左端走,找比key大的值,将该值放在坑位处,更新hole为右端找到的数的位置,形成新的坑位。然后左右继续找,直至左右相遇就停止,此时相遇点为坑位,将key值放入。这样一趟查找下来待排序key的左部分均比key小,右部分均比key大。再对左右两部分重复此操作,知到序列有序。
挖坑法代码实现
int PartSort2(int* a, int begin, int end)
{
int mid = GetMid(a, begin, end);
Swap(&a[mid], &a[begin]);
int key = a[begin];
int hole = begin;
while (begin < end)
{
while (begin < end && a[end] >= key)
{
end--;
}
a[hole] = a[end];
hole = end;
while (begin < end && a[begin] <= key)
{
begin++;;
}
a[hole] = a[begin];
hole = begin;
}
a[hole] = key;
return key;
}
3.前后指针
通过控制前后两个指针来达到排序目的的方法。
前后指针版算法实现
int PartSort3(int* a, int begin, int end)
{
int mid = GetMid(a, begin, end);
Swap(&a[mid], &a[begin]);
int key = begin;
int prev = begin;
int cur = prev + 1;
while(cur <= end)
{
if (a[cur] < a[key])
{
prev++;
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[key]);
return prev;
}
非递归实现快速排序
采用我们之前实现的数据栈来实现,将要处理的区间压栈,取出区间,一次排序后得到key,再将[left,key - 1]和[key + 1,right]依次入栈。重复以上操作。当区间不存在或者区间只有一个遇元素不需要将区间压栈,直接重复。直到栈空时排序结束,序列达到有序。
非递归代码实现
void QuickSortNonR(int* a, int begin, int end)
{
Stack s;
StackInit(&s);
//将[begin,end]压栈 先入右,后入左
StackPush(&s, end);
StackPush(&s, begin);
while (!StackEmpty(&s))
{
int left = StackTop(&s);
StackPop(&s);
int right = StackTop(&s);
StackPop(&s);
int key = PartSort2(a, left, right);
if (key + 1 < right)
{
StackPush(&s,end);
StackPush(&s, key + 1);
}
if (left < key - 1)
{
StackPush(&s, key - 1);
StackPush(&s, begin);
}
}
StackDestroy(&s);
}
总结
1.快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2.时间复杂度:O(N*logN)
3.空间复杂度:O(1)
4.稳定性:不稳定
以上就是我对交换排序中两类排序的理解,谢谢观看,欢迎大家提出问题,一起进步!