https://wiki.jikexueyuan.com/project/easy-learn-algorithm/fast-sort.html
这篇文章说一下快速排序,3路快速排序,稳定快速排序,以及非递归版本的快速排序怎么写。
不懂快速排序的朋友可以点开看一下上面那一个链接,里面有介绍快速排序的概念的。
首先我们来看一下普通快速排序:
普通快速排序:
话不多说先上代码:
public static void main(String[] args) {
int[] nums = {6,0,3,1,2,4,4,6,7,8,};
// for (int i = 0; i < nums.length; i++) {
// nums[i] = (int)(Math.random() * 10);
// }
quickSort(nums, 0, nums.length - 1);
for (int num : nums) {
System.out.print(num+",");
}
}
//快速排序
public static void quickSort(int[] array, int left, int right) {
if (left < right) {
// mid的值是在 partition之后返回的已经安置好位置的那个元素的位置,这个值接下来被用于作为下面两个递归的左边界和右边界
int mid = partition(array, left, right);
//递归排序mid左边那部分
quickSort(array, left, mid - 1);
//递归排序mid右边那部分
quickSort(array,mid + 1, right);
}
}
public static int partition(int[] array, int left, int right) {
//以左边第一个值作为基准值
int pivot = array[left];
int l = left;
int r = right;
//这个循环的作用是 让从array[lef] 到 array[right] 间到元素 左边部分的元素小于 pivot 右边部分的元素大于pivot
while(l < r){
//找到右边第一个小于 pivot的元素
while (l < r && array[r] >= pivot){
r--;
}
//找到左边第一个小于pivot的元素
while (l < r && array[l] <= pivot) {
l++;
}
//交换这两个元素
swap(array, l, r);
}
// array[lef] 到 array[right] 的元素处理完毕后,让pivot元素到它该有到位置,交换pivot和上一个循环截止到那个元素
// 注意这里 被交换的元素 array[l] 和 array[r]是同一个元素 l和r已经相等了
//能够交换 只因为 array[l]一定小于等于pivot 这也是为什么上一个大循环中 里面的第一个while循环是 从r开始判断,如果从l开始判断那么最后
//中止的值是大于等于pivot的
array[left] = array[l]; // array[left] = array[r]也可以 因为 l和r已经相等了
array[l] = pivot;
return l;
}
//交换元素
public static void swap(int[] array, int l, int r) {
int temp = array[l];
array[l] = array[r];
array[r] = temp;
}
上面那段代码就是普通快速排序, 每次从将要partition的数组区域里面选最左的元素为pivot(基准值) 然后对这段数组区域进行 partition,partition之后的这段数组区域,左边部分小于pivot,右边部分大于pivot,同时pivot 也找到了排序后终身要要放置的位置 并且被放置到了这个位置(终身指的意思是partition之后pivot元素在整个quicksort变得整体有序之后一辈子要放的位置,所以,一次partion就可以 确定一个元素的位置) 注意⚠️:上面这段代码我用最左边的元素当作pivot 当然也可以拿最右边的元素当作pivot,同时也能拿任意一个元素当作pivot, 但是拿最左边和最有边元素当作pivot的话代码边界条件判断比较少,读者可以去网上找一找拿任意元素当pivot的代码,普通快速排序中用最左最右元素当作pivot边界判断少。 但是注意⚠️: 下面我介绍的3路快速排序中:拿任意一个位置的元素当pivot, 代码边界的判断都是一样的。
三路快速排序:
三路快速排序和普通快速排序的主要区别是 在partiontion阶段的不同。
一段数组里面可能出现重复元素,普快的partition在上文中提到了每次只能找到一个pivot的精准定位,重复元素也要再判断一次,但是三路快速排序的partition优化了,它不需要再判断重复元素,在一次partition中返回pivot元素在这段数组区域的最左和最右临界下标. 举个例子: 4为pivot,数组 array[1,2,3,4,5,6,8,4,4,6] 在一次partition后数组会变成下面这个样子
array[3,2,1,4,4,4,5,8,6]
返回的就是 3 和5. array[3]前面的元素比4都小, array[5]后面的元素比4 大
在3路快排的一次partion后 比如说partition返回了[3, 5] 那么再对(left, 3- 1)和(5 + 1, right)进行递归排序, 具体 partion的算法内容 和荷兰国旗问题的方法一摸一样,可参考
漫画:常考的荷兰国旗问题你还不会吗?(初级) - 云+社区 - 腾讯云
不多说上代码:
//三指针 l指向left - 1意义为 小于pivot的数组段段右边界, r初始化为right+1 意义为大于pivot段数组段段左边界
//一次遍历得到l 和 r 段最终值, l 和 r之间段元素 为值等于pivot 段元素
public static int[] partitionHeLanGuoQi(int[] array, int left, int right) {
int pivot = array[left];
int l = left - 1;
int r = right + 1;
int cur = left;
while (cur < r) {
if (array[cur] < pivot) {
swap(array, ++l, cur++);
} else if (array[cur] > pivot) {
swap(array, --r, cur);
} else {
cur++;
}
}
return new int[]{l, r};
}
public static void quickSort3(int[] array, int left, int right) {
if (left < right) {
int[] partitionResult = partitionHeLanGuoQi(array, left, right);
quickSort(array, left, partitionResult[0]);
quickSort(array, partitionResult[1],right);
}
}
稳定快速排序:
不多说先上代码:
public static int partitionStable(int[] array, int left, int right) {
int[] temp = new int[right - left + 1];
//制定最左元素为pivot
int pivot = array[left];
int idxForTemp = 0; // 初始化指向临时数组temp 的第一个位置
//先在 left 到right 这个范围内找小于 pivot 到元素
for (int i = left + 1; i <= right; i++) {
if (array[i] < pivot) {
temp[idxForTemp++] = array[i];
}
}
temp[idxForTemp++] = pivot; // 把 pivot放到它该有到位置上面
int awsFromTemp = idxForTemp - 1; // 保存放pivot到位置 后面要用
//再遍历一遍数组找到所有大于等于pivot到元素 依次放到临时数组temp中
for (int i = left + 1; i <= right; i++) {
if (array[i] >= pivot) {
temp[idxForTemp++] = array[i];
}
}
idxForTemp = 0;
int aws = 0;
//把排好序到部分拷贝回原数组
for (int i = left; i <= right; i++) {
if(idxForTemp == awsFromTemp){
aws = i;
}
array[i] = temp[idxForTemp++];
}
return aws;
}
public static void sort(int[] nums, int left, int right) {
if (left < right) {
int i = partitionStable(nums, left, right);
sort(nums, left, i - 1);
sort(nums, i + 1, right);
}
}
快速排序一般来讲是不稳定的,这是因为每一次partition 是在一个数组本身上通过左右元素交换操作而达到前半部分小,后半部分元素大,而在普通快速排序中 partition里大循环里面的两个小循环条件包含等于pivot的时候停下,所以当等于pivot的时候再进行交换,就会造成不稳定。
解决办法就是:在一次partition中,创建一个临时数组,在要partition的数组里找到小于pivot的元素添加到临时数组中,然后将 pivot添加到临时数组中, 然后再扫描一遍数组将大于等于pivot到元素添加到临时数组中,最后再将临时数组拷贝回原数组,这样partition之后 到快速排序就是稳定的。
非递版归快速排序:
先不说 上代码:
//利用栈先进后出的特性存入 即将要partition的边界条件 即 left 和right 模拟递归进行快排
public static void quickSortWithoutRegression(int[] nums) {
//创建一个数组 保存每次要进行partition时候的left和right
Stack<int[]> stack = new Stack<>();
stack.push(new int[]{0, nums.length - 1});
// 开始进行排序
while (!stack.isEmpty()) {
//出栈 即将新出的left 和right 搞出来
int[] pop = stack.pop();
int left = pop[0];
int right = pop[1];
int[] tpReturned;
if (left < right) {
tpReturned = partitionInNORegression(nums, left, right);
stack.push(new int[]{left, tpReturned[0]});
stack.push(new int[]{tpReturned[1], right});
}
}
}
//partition可以用3路快排的partition 也可以用普通的partition,也可以用稳定版本的partition,这里用3路版本的partition
public static int[] partitionInNORegression(int array[], int left, int right) {
int l = left - 1;
int r = right + 1;
int cur = left;
int pivot = array[left];
while (cur < r) {
if (array[cur] < pivot) {
swap(array,++l,cur++);
} else if (array[cur] > pivot) {
swap(array, --r,cur);
}else {
cur++;
}
}
return new int[]{l, r};
}
非递归版本的快速排序就是用栈保存partition时候的边界变量,然后每次partition结束后返回的值再和left,right 入栈作为下一次partition的边界变量(即left 和right)。