『算法』快速排序

本篇博客主要介绍一下快速排序的相关算法。

快速排序


我们都知道快速排序在所有的排序算法中效率是很高的。时间复杂度为 O ( l o g 2 N ) O(log_2N) O(log2N),空间复杂度为 O ( l o g 2 N ) O(log_2N) O(log2N)

快速排序的基本思路如下:

  1. 从待排序区间选择一个数,作为基准值(pivot)
  2. Partition:遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边
  3. 采用分治思想对左右两个小区间按照同样的方式处理,直到小区间中元素的数量小于2个

我们来看一下整体代码框架

public class QuickSort {
    public static void quickSort(int[] arr) {
        qSort(arr, 0, arr.length - 1);
    }

    private static void qSort(int[] arr, int left, int right) {
    	// 区间中元素小于2个,已经有序
        if (left >= right) {
            return;
        }

		// 对区间进行整理,将区间中的元素以一个基准值分为左右两个部分
		// 左边的部分比基准值小,右边的部分比基准值大;并返回基准值的下标
        int pivotIndex = partition(arr, left, right);
        
        // 对左区间进行整理
        qSort(arr, left, pivotIndex - 1);
        // 对右区间进行整理
        qSort(arr, pivotIndex + 1, right);
    }

	// 对区间进行整理,并返回基准值所在下标
    private static int partition(int[] arr, int left, int right) {
    }

	// 交换
	private static void swap(int[] arr, int i, int j) {
	    int tmp = arr[i];
	    arr[i] = arr[j];
	    arr[j] = tmp;
	}
}

区间的整理,即partition函数有三种写法,下面,我们分别来看一下。

hoare法


private static int partition(int[] arr, int left, int right) {
	// 取最左边的元素作为基准值
    int i = left, j = right, pivot = arr[left];
    while (i < j) {
    	// 先从后往前找到比基准值小的值
        while (i < j && arr[j] >= pivot) {
            --j;
        }
        // 再从前往后找到比基准值大的值
        while (i < j && arr[i] <= pivot) {
            ++i;
        }
        swap(arr, i, j);
    }
    
    swap(arr, i, left);

    return i;
}

上述代码,我们有一个疑问,我们如何知道最外层的while循环结束后,i下标的元素一定比left下标的元素小,因为我们交换完之后,需要保证区间满足i下标左边的元素都比基准值小,右边的元素都比基准值大
这里,我们看一下代码,while结束有两种情况

  • 在第7行不满足条件i < j而退出的循环,因为++i导致循环退出,由于前面–j的操作已经结束,所以j下标的元素一定比基准值小,而++i退出循环时,i和j是相等的,所以这种情况可以保证i下标的元素一定比left下标的元素值要小;
  • 在第5行不满足条件i<j而退出的循环,因为--j导致循环退出,前面已经完成了一次i下标元素和j下标元素的交换,所以此时i下标元素的值一定比基准值小,所以因为--j退出循环,i和j是相等的,所以这种情况可以保证j下标的元素一定比left下标的元素值要小。

但是,如果我们将++i和–j这两个代码块的顺序调换一下,这个代码就是错的,因为无法保证上述问题。所以,hoare有两种写法

  • 如果我们取最左边的值作为基准值,就要先从右向左找,再从左向右找
  • 如果我们取最右边的值作为基准值,就要先从左向右找,再从右向左找

另一种写法如下:

private static int partition(int[] arr, int left, int right) {
    int i = left, j = right, pivot = arr[right];

    while (i < j) {
        while (i < j && arr[i] <= pivot) {
            ++i;
        }
        while (i < j && arr[j] >= pivot) {
            --j;
        }
        swap(arr, i, j);
    }

    swap(arr, right, i);

    return i;
}

挖坑法


private static int partition(int[] arr, int left, int right) {
    int i = left, j = right, pivot = arr[left];

    while (i < j) {
        while (i < j && arr[j] >= pivot) {
            --j;
        }
        arr[i] = arr[j];
        while (i < j && arr[i] <= pivot) {
            ++i;
        }
        arr[j] = arr[i];
    }
    arr[i] = pivot;

    return i;
}

同样的,挖坑法也需要注意

  • 如果选取最左边的元素作为基准值,则需要先从右向左找再从左向右找
  • 如果选取最右边的元素作为基准值,则需要先从左向右找再从右向左找

下面是另一种写法

private static int partition(int[] arr, int left, int right) {
    int i = left, j = right, pivot = arr[right];

    while (i < j) {
        while (i < j && arr[i] <= pivot) {
            ++i;
        }
        arr[j] = arr[i];
        while (i < j && arr[j] >= pivot) {
            --j;
        }
        arr[i] = arr[j];
    }
    arr[i] = pivot;

    return i;
}

前后指针法


该种方法出自《算法导论》,我们先来看一下代码:

private static int partition(int[] arr, int left, int right) {
    int d = left - 1;
    int pivot = arr[right];
    
    for (int i = left; i < right; ++i) {
        if (arr[i] < pivot) {
            ++d;
            swap(arr, i, d);
        }
    }
    
    swap(arr, d + 1, right);

    return d + 1;
}

快速排序的非递归实现


import java.util.Arrays;
import java.util.Stack;

public class QuickSort {
    public static void main(String[] args) {
        int[] arr = {
                1, 3, 5, 7, 9,
                2, 4, 6, 8, 0
        };

        quickSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void quickSort(int[] arr) {
        Stack<Integer> stack = new Stack<>();
        stack.push(arr.length - 1);
        stack.push(0);

        while (!stack.isEmpty()) {
            int left = stack.pop();
            int right = stack.pop();
            if (left >= right) {
                continue;
            }

            int pivotIndex = partition(arr, left, right);

            stack.push(pivotIndex - 1);
            stack.push(left);

            stack.push(right);
            stack.push(pivotIndex + 1);
        }
    }

    private static int partition(int[] arr, int left, int right) {
        int i = left, j = right, pivot = arr[left];

        while (i < j) {
            while (i < j && arr[j] >= pivot) {
                --j;
            }
            arr[i] = arr[j];
            while (i < j && arr[i] <= pivot) {
                ++i;
            }
            arr[j] = arr[i];
        }
        arr[i] = pivot;

        return i;
    }
}

在这里插入图片描述

Python一行代码实现快速排序


我们来看一个高逼格代码,Python一行完成快速排序

quick_sort = lambda array: array if len(array) <= 1 else quick_sort([item for item in array[1:] if item <= array[0]]) + [array[0]] + quick_sort([item for item in array[1:] if item > array[0]])

print(quick_sort([2,5,9,3,7,1,5]))

运行结果如下
在这里插入图片描述

快速排序的缺陷和优化方法


上述版本的快速排序方法具有三个缺点

  • 逆序,表现不佳,无法达到分治的效果;
  • 基准值选的不好,也无法达到分治的效果;
  • 元素数量非常多,会导致递归深度过深,有可能栈就溢出了

优化办法

  • 优化基准值的选取方式,三元素取中(第一个元素、最后一个元素、中间元素)
  • 如果递归深度达到一定层次之后,就不再继续递归,而是对当前待排序区间进行其他排序,如:堆排序
  • 如果当前待排序区间已经比较小了,直接使用插入排序

C++中的std::sort就是这么进行优化的
Java中的Collections.sort也是基于快速排序,但是它会根据数据的各种特点,会做出各种各样的优化,还会在有些特定情况下变成TimSort(改进版的归并排序)

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值