快排是一种基于分治思想的排序算法,在如此众多的排序算法中,其综合排序速度最佳(大多情况为O(nlog2n)),这也是快排之所以叫快排的一个原因之一。先来看一下快排的基本轮廓。
//快排函数
static void quickSort(int[] arr, int begin, int end) {
if (begin < end) {//分治区间大于2就继续划分
//调用快排的内核函数partition确定主元位置,并将主元放到最终位置
int pivot_loc = partition2(arr, begin, end);
//分治左区间
quickSort(arr, begin, pivot_loc - 1);
//分治右区间
quickSort(arr, pivot_loc + 1, end);
}
}
为什么快排会做到如此快速呢?从以上代码中可以发现,这是因为快排的每一次划分决定的,它的每次划分尽量做到从中间切开,使得在尽量少的划分次数下把原数组划分成最小的单元。划分函数的作用是把选定的主元放到最终的合适位置,并且保证主元位置的左边元素全部小于等于主元,右边元素全部大于主元,最终返回主元的位置。
一 partition划分函数
(1)单向扫描分区法
思想:这种划分是选定主元后,确定左右两个“指针”扫描待排数组,每一次循环都是判断左边指针所指元素是否小于等于主元,若是,则把左指针向右移一位,否则交换左右指针所指元素,并且右指针向左移动一步。直到左右指针交错后退出循环,这时右指针所指位置即为主元的最终位置,交换右指针所指元素与主元所指元素,返回主元所在下标即可。
//1.单向扫描分区法(左指针主动,右指针被动)
static int partition1(int[] arr, int begin, int end) {
int pivot = arr[begin];//确定主元为数组的第一个元素(默认)
int left = begin + 1;//(左侧)扫描指针
int right = end;//右侧指针
while (left <= right) {
if (arr[left] <= pivot) {//扫描元素小于等于主元,左指针向右移动
left++;
} else {//扫描元素大于主元,两指针元素交换,右指针左移
swap(arr, left, right);
right--;
}
}
//左右指针交错后,主元找到最终位置(即:right所处位置)
swap(arr, begin, right);//把主元放到最终位置
return right;//返回主元位置
}
(2)双向扫描分区法
思想:这种划分方法左右指针的地位更加平等,选定主元后,左指针在右指针的左边的前提下,当左指针所指元素小于等于主元时,就不断向右推进。右指针也是同理,当其所指元素大于主元时,就不断向左推进,直到左右指针相交便找到了主元的最终位置。这里需要注意的是,当左右指针各自在向对方方向推进的时候,不知道是否相交了,所以在推进指针的内层while循环中要加判断条件:左指针在右指针的左边。
//2.双向扫描分区法(两指正等地位,向中间扫描)
static int partition2(int[] arr, int begin, int end) {
getMid2(arr,begin,end);
int pivot = arr[begin];//确定主元为数组的第一个元素
int left = begin + 1;//左侧指针
int right = end;//右侧指针
while (left <= right) {
//注意加left<=right条件,防止左右指针咋在外部while的内部相交
while (left <= right && arr[left] <= pivot) left++;
while (left <= right && arr[right] > pivot) right--;
//注意:交换左右指针所指元素时也要判定left<=right
//因为在上面两个while中可能导致left和right错位
if (left <= right) swap(arr, left, right);
}
//退出while表示已经相交,找到了pivot的最终位置:right位置(由arr[left]<=pivot决定)
swap(arr, begin, right);
return right;//返回位置
}
partition函数虽然是快排的“内核”,但真正影响快排效率的是输入数字的特征和主元的选取,数字特征我们没法控制,但我们可以通过控制主元的选取方法来控制数字特征带来的影响。
pivot主元的选取
(1)三点中值法
思想:这种选取主元的方法是选取待排数组中首,尾和中间位置这三个元素中第二大的元素作为主元,这样可以大概率的避免划分失衡带来的效率影响。
//三点中值法
static void getMid1(int[] arr, int begin, int end) {
int midIndex = (begin + end) / 2;//中间下标
int midValueIndex = -1;//中值的下标
if (arr[begin] <= arr[midIndex] && arr[begin] >= arr[end]) {
midValueIndex = begin;
} else if (arr[end] <= arr[midIndex] && arr[end] >= arr[begin]) {
midValueIndex = end;
} else {
midValueIndex = midIndex;
}
//把中值放在数组的首位,便于取主元
swap(arr, begin, midValueIndex);
}
(2)绝对中值法
思想:把待排序数字序列分成五个一组(最后一组除外),把它们进行排序后选出各组的中值,再将这些中值排序后再一次选出一个最终中值作为主元。这种选取主元的方式可以保证时间复杂度始终稳定在O(nlog2n)而不含有运气成分。
//绝对中值法
static int getMid2(int[] arr, int begin, int end) {
int size = end - begin + 1;//数组长度
//每五个元素一组
int groupSize = (size % 5 == 0) ? (size / 5) : (size / 5 + 1);//组数
//存储各小组的中值
int medians[] = new int[groupSize];
int indexOfMedians = 0;//medians[]数组的下标
for (int i = 0; i < groupSize; i++) {
//单独处理最后一组,因为可能不满五个元素
if (i == groupSize - 1) {
//此处应该单独写一个插入排序,这样调接口(偷懒)
Arrays.sort(arr, begin + 5 * i, end);//排序最后一组
medians[indexOfMedians++] = arr[(begin + i * 5 + end) / 2];//最后一组的中间那个
} else {
Arrays.sort(arr, begin + 5 * i, begin + i * 5 + 4);//排序非最后一组
medians[indexOfMedians++] = arr[begin + i * 5 + 2];//当前组排序后的中间那个
}
}
//对所有中值再排序取中值
Arrays.sort(medians, 0, medians.length - 1);
return medians[medians.length/2];
}
附录:一份完整的快排代码
import java.util.Scanner;
public class T1快速排序 {
public static void main(String[] args) {
//输入"任意"长度的数组,以空格分隔,以回车结束输入
Scanner sc = new Scanner(System.in);
String[] str_array = sc.nextLine().toString().split(" ");
int[] array = new int[str_array.length];
for (int i = 0; i < array.length; i++) {
array[i] = Integer.valueOf(str_array[i]);
}
//快速排序
quickSort(array, 0, array.length - 1);
//输出排序后的数组
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
//快排函数
static void quickSort(int[] arr, int begin, int end) {
if (begin < end) {//分治区间大于2就继续划分
//调用快排的内核函数partition确定主元位置,并将主元放到最终位置
int pivot_loc = partition(arr, begin, end);
//分治左区间
quickSort(arr, begin, pivot_loc - 1);
//分治右区间
quickSort(arr, pivot_loc + 1, end);
}
}
//双向扫描分区法(两指正等地位,向中间扫描)
static int partition(int[] arr, int begin, int end) {
getMid(arr, begin, end);
int pivot = arr[begin];//确定主元为数组的第一个元素
int left = begin + 1;//左侧指针
int right = end;//右侧指针
while (left <= right) {
//注意加left<=right条件,防止左右指针咋在外部while的内部相交
while (left <= right && arr[left] <= pivot) left++;
while (left <= right && arr[right] > pivot) right--;
//注意:交换左右指针所指元素时也要判定left<=right
//因为在上面两个while中可能导致left和right错位
if (left <= right) swap(arr, left, right);
}
//退出while表示已经相交,找到了pivot的最终位置:right位置(由arr[left]<=pivot决定)
swap(arr, begin, right);
return right;//返回位置
}
//交换数组中的两个元素
static void swap(int[] arr, int index1, int index2) {
int temp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = temp;
}
//三点中值法
static void getMid(int[] arr, int begin, int end) {
int midIndex = (begin + end) / 2;//中间下标
int midValueIndex = -1;//中值的下标
if (arr[begin] <= arr[midIndex] && arr[begin] >= arr[end]) {
midValueIndex = begin;
} else if (arr[end] <= arr[midIndex] && arr[end] >= arr[begin]) {
midValueIndex = end;
} else {
midValueIndex = midIndex;
}
//把中值放在数组的首位,便于取主元
swap(arr, begin, midValueIndex);
}
}