冒泡排序
实现思想(升序):将数组最大值一步一步交换到数组末尾,该值完成排序,需排序区间-1,继续找需排序区间最大值交换到末尾,直到需排序区间只有一个数,整个数组就有序了。
动图演示
void Swap(int* a, int* b)//交换函数
{
int tmp = *a;
*a = *b;
*b = tmp;
}void BubbleSort(int* a, int n)
{
int i = 0;
int index = n - 1;//最后一个值的下标
while (index > 0)
{int flag = 0;//记录该区间是否有值交换
for (i = 0; i < index; i++)//将需排序区间里面最大的值交换到区间最后一个
{
if (a[i] > a[i + 1])//大的往后交换
{
Swap(&a[i], &a[i + 1]);
}
}if (flag == 0)//表示没有值交换,数组已经有序,前面待排序区间也已经有序
{
return;
}
index--;//冒泡排序进行一次,区间向前缩进1,后面的数已经完成排序。
}
}
时间复杂度O(N^2) 空间复杂度O(1)
快速排序
基本思想:任取待排序元素序列中的某元素作为基准值,按照该基准值将待排序列分为两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右序列重复该过程,直到所有元素都排列在相应位置上为止。
按照基准值将待排序列分为两子序列,常见的方式有:
1、Hoare版本
2、挖坑法
3、前后指针法
递归实现(升序)
Hoare版本
单趟的动图演示
Hoare版本的单趟排序基本思想
如果基准值key在左边,右边先开始找比key小的值,找到了停止,左边才开始找比key大的值,找到后交换,重复此过程,直到左右相遇,将相遇点的值与key的值交换,这样单趟排序完成,key左边的值比key小,key右边的值比key大。
然后我们在将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,排序完成。
int SingleSort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])//右边找到小于keyi位置的值停止
{
right--;
}
while (left < right && a[left] <= a[keyi])//左边找到大于keyi位置的值停止
{
left++;
}
Swap(&a[left], &a[right]);//交换
}
//当left=right时,表示相遇,与keyi位置交换
Swap(&a[keyi], &a[left]);
return left;
}void QuickSort(int* a, int left,int right)
{
int keyi = SingleSort1(a,left,right);
if (left >= right)//表示区间只有一个或者没有值,直接返回
{
return;
}
//[left,keyi-1] keyi [keyi+1,right]
QuickSort(a, left, keyi - 1);//递归keyi的左区间
QuickSort(a, keyi + 1, right);//递归keyi的右区间
}
挖坑法
单趟的动图演示
挖坑法单趟排序基本思想 :pit位置存一般存最左边或者最右边的值(区别是pit在左,右边先找,pit在右,左边先找)这里pit在左,右边找比pit小的,找到了,将左边的值填入,此时右边形成新的坑,左边开始找,找到比pit大的,填入到右边的坑,知道左右相遇停止,将pit填入相遇点。
//挖坑法
int SingleSort2(int* a, int left, int right)
{
int pit = a[left];
while (left < right)
{
while (left < right && a[right] >= pit)
{
right--;
}
//找到比pit小的值,填到左边坑位上,此时坑在右边
a[left] = a[right];
while (left < right && a[left] <= pit)
{
left++;
}
//找到比pit大的值,填到右边坑位上,此时坑在左边
a[right] = a[left];
}
a[left] = pit;//将pit填入到相遇点
return left;
}void QuickSort(int* a, int left,int right)
{
int keyi = SingleSort2(a,left,right);
if (left >= right)
{
return;
}
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
前后指针法
单趟的动图演示
前后指针法单趟排序基本思想:选取一个key(这里在最左边) 前指针prev初始在key位置上,后指针cur初始在key+1位置上,cur开始向后找比key小的值,找到了,prev先++,在交换两个的值(如果++prev=cur表示的是自己跟自己交换,可以不处理),cur继续向后找,直到cur出了数组的范围,交换prev与key的值,单趟排序完成。类比两个指针赶着大的值向后走,小的留在前面。
int SingleSort3(int* a, int left, int right)
{
int keyi = left;
int prev = left;//前指针
int cur = left + 1;//后指针
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)//表示cur找到比key小的值,并且不是自己于自己交换
{
Swap(&a[cur], &a[prev]);
}
cur++;//cur大于key的值,交换后继续往后找都会执行++操作
}
Swap(&a[prev], &a[keyi]);//cur出了数组范围,交换prev与keyi的值
return prev;
}
void QuickSort(int* a, int left,int right)
{
int keyi = SingleSort3(a,left,right);
if (left >= right)
{
return;
}
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
快排的优化
三数取中
快速排序的时间复杂度是O(NlogN),是理想情况下计算的结果。每次进行完单趟排序后,key的左序列与右序列的长度都相同:
选取的key不可能都是正中间的那个数,当待排序列本就是一个有序的序列时,我们若是依然每次都选取最左边或是最右边的数作为key,那么快速排序的效率将达到最低:
此时快速排序的时间复杂度退化为O(N2)。所以,对快速排序效率影响最大的就是选取的key,若选取的key越接近中间位置,则效率越高,所以出现三数取中优化。
三数取中:最左边的数、最右边的数以及中间位置的数。三数取中就是取这三个数当中,值的大小居中的那个数作为该趟排序的key。
//三数取中 左中右返回中间的那个
int MidIndex(int* a, int left, int right)
{
int mid = left + (right - left)/2;//取到中间位置下标 (left+right)/2 (left+right)要是超过整型最大值会溢出
if (a[mid] < a[left])
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[right] < a[left])
{
return right;
}
else
{
return left;
}
}
else
{
if (a[left] > a[right])
{
return left;
}
else if (a[mid] > a[right])
{
return right;
}
else
{
return mid;
}
}
}
当大小居中的数不在序列的最左或是最右端时,我们不是就以居中数的位置作为key的位置,而是将key的值与最左端的值进行交换,这样key就还是位于最左端了,所写代码就无需改变,而只需在单趟排序代码开头加上以下两句代码即可:
int mid = MidIndex(a, begin, end);//获取大小居中的数的下标
Swap(&a[begin], &a[mid]);//将该数与序列最左端的数据交换,这样key还是从最左边开始
//以下代码保持不变...
时间复杂度O(N*log^N) 空间复杂度O(1)