快速排序:
同冒泡排序一样,快速排序也属于交换排序。通过元素之间的比较和交换位置来达到排序的目的。
不同的是,冒泡排序在每一轮只把一个元素冒泡到数列的一端,而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列的一边,比它小的元素移动到数列的另一边。从而把数列拆分成为两部分。
这种思路叫做分治法。
如图所示,在分治法的思想下,原始数列在每一轮被拆分成两部分,每一步分又在下一轮被拆分成两部分,直到不可再分为止。
这样一来,平均情况下只需要logn轮,因此排序算法的平均时间复杂度为O(logn)。
快速排序算法的核心就在于基准元素的选择和如何把其他元素移动到基准元素两侧。
基准元素的选择:
基准元素,英文名pivot,用于在分治过程中以此为中心,把其他元素移动到基准元素的两侧。
最简单的方式,就是选择数列的第一个元素。
这种情况,在大多数情况下是没有问题的。但是,如果原本是一个逆序的数列,期望排成顺序数列,就会出现如下效果:
整个数列并没有被分成一半一半,每次只确定了一个元素的位置。
在这种极端情况下,快速排序退化成了n轮,时间复杂度退化成O(n^2)。
为了避免这种情况发生,我们可以不选择数列的第一个元素,而是随机选择一个元素作为基准元素。
这样一来,即使是在数列完全逆序的情况下,也可以有效地将数列分成两部分。当然,即使是随机选择基准元素,每一次也有极小的几率选到数列的最大值或最小值,同样会影响到分治的效果。
所以呀,快速排序的平均时间复杂度是O(nlogn),最坏情况下的时间复杂度是O(n^2)。
元素的移动:
挖坑法,交换法
挖坑法的实现过程:
给定原始数列如下,要求从小到大排序:
第一步:选取基准元素。并记录这个位置index,这个位置相当于一个坑。并且设置两个指针left和right,分别指向数组两端。
第二步:从right位置开始,把指针指向元素与基准值作比较。如果该元素大于基准值,那么 right左移。直到找到小于pivot值的元素。如果该元素小于基准值,那么将该值放入坑中,然后right指针指向的位置就变成了新的坑。
在当前数列中,6>5,所以right指针左移,坑的位置不变。
此时,right右边的区域代表大于基准值的区域。
继续比较,此时,right指针指向的值1<5,将1填入坑中,并且1所在位置处变成新的坑。
left指针前面的区域代表小于基准值的区域。
第三步:从left位置开始,比较left位置处的数据值与pivot的值大小关系,如果该元素小于基准值,那么 left右移。直到找到第一个大于基准值的元素。如果该元素大于pivot,那么将该值放入坑中,并将left位置定义为新坑。
当前数列中,9>5,所以把9填入坑中。right指针左移。这时候,元素9所在位置就变为了新的坑。
接下来,我们切换到right指针,7>5,right指针左移。
因为没有进行填坑操作,所以不必进行指针的切换。
4<5,将4入坑,left指针右移。将4原来位置变为新坑。
接下来,我们切换到left指针,3<5,left指针右移。
2<5,left指针右移。
此时,left指针和right指针重合,然后将Pivot的值填入坑中即可。
代码实现:
#include<stdio.h>
#include<Windows.h>
//挖坑法
int QuickSortCore(int *arr, int len)
{
int povit = *arr;
int index = 0;
int left = 0;
int right = len - 1;
while (left < right)
{
while (left<right)
{
if (arr[right] < povit)
{
arr[index] = arr[right];
left++;
index = right;
break;
}
else{
right--;
}
}
while (left<right)
{
if (arr[left] > povit)
{
arr[index] = arr[left];
right--;
index = left;
break;
}
else{
left++;
}
}
}
arr[index]=povit;
return index;
}
void QuickSort(int* arr, int len)
{
/*快速排序:
函数参数要包含数列的头指针和数列中要处理数据的长度。
判断数列是否为空,或者只有一个元素。为空,返回
不为空:将数列进行排序:1)选定数列的第一个元素做基准点povit,记录坑的位置index,定义两个变量 left,right分别从数列两端开始,
2)从right向左查找,找到第一个小于povit的元素,将该值入坑,left++,并将该值所在位置定义为新坑。
3)从left向右查找,找到第一个大于povit的元素,将该值入坑,right--,并将该值所在位置定义为新坑。
4)重复2,3,直到left==right,此时将基准点值入坑即可。
以基准点所在位置(index)为边界,将数列划分成为两部分,递归的处理数列的两部分。
进行返回。
*/
if (arr == NULL || len == 1||len==0)
{
return;
}
int index=QuickSortCore(arr, len);
QuickSort(arr, index);
QuickSort(arr+index + 1, len - index-1);
return;
}
void ArrPrintf(int* arr,int len)
{
if (arr == NULL)
{
printf(" NULL \n");
return;
}
int i = 0;
for (; i < len; i++)
{
printf(" %d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 5, 9, 3, 2, 4, 7, 1, 6 };
int len = sizeof(arr) / sizeof(arr[0]);
QuickSort(arr, len);
ArrPrintf(arr,len);
system("pause");
return 0;
}
指针交换法:
1)定义两个变量,left right分别表示数列的第一个和最后一个元素下标。
2)选定数列第一个值作为基准值,povit=arr[0]
3)从right开始,向前查找,直到找到第一个小于povit的值,交换arr[left],arr[right],并将left++
4)从left开始,向后查找,直到找到第一个大于povit的值,将arr[left],arr[right]交换。并将right++.
5)重复3,4步骤,直到left与right重合。
//指针法
/*快速排序:
函数参数要包含数列的头指针和数列中要处理数据的长度。
判断数列是否为空,或者只有一个元素。为空,返回
不为空:将数列进行排序:
1)定义两个变量left right分别表示数列的左右两个边界。选取数列的第一个元素作为基准点povit。
2)从right开始往左查找,找到第一个小于基准点的元素,将 arr[right] arr[left]进行交换,left--.
3)从left开始往右查找,找到第一个大于基准点的元素,将arr[right] arr[left]进行交换,right++;
4)循环执行2 3两步,直到left与right重合。
5)right==left的位置就是基准点所在的位置。
以基准点所在位置(index)为边界,将数列划分成为两部分,递归的处理数列的两部分。
进行返回。
*/
int QuickSortSwapCore(int *arr, int len)
{
int left = 0;
int right = len - 1;
int tmp = 0;
int povit = arr[0];
while (left < right)
{
while (left < right)
{
if (arr[right] < povit)
{
tmp = arr[right];
arr[right] = arr[left];
arr[left] = tmp;
left++;
}
else{
right--;
}
}
while (left < right)
{
if (arr[left]>povit)
{
tmp = arr[right];
arr[right] = arr[left];
arr[left] = tmp;
right++;
}
else{
left++;
}
}
}
return left;
}
void QuickSortSwap(int *arr, int len)
{
if (arr == NULL || len == 0 || len == 1)
{
return;
}
int index = QuickSortSwapCore(arr, len);
QuickSortSwap(arr, index);
QuickSortSwap(arr + index + 1, len - index - 1);
}