一个快速排序,一个堆排序,一个归并排序,全部被张三防出去了啊

173 阅读3分钟

在经典的八大排序算法中,由于快速排序、堆排序和归并排序的时间复杂度可以达到O(nlogn),且复杂性较高,所以是面试中比较常问的点,今天我们一起来复习一下这三种排序算法。

说明:本文中所有的排序都是按升序进行排序

归并排序

基本思路:借助额外空间,将两个较短数组合并为一个更长的有序数组,整个过程递归执行,直到得到数组的长度等于原数组长度

算法思想:分治思想

算法过程:

  • 首先对数组进行等长划分,整个过程递归执行,直到子数组长度全部为1
  • 对两个子数组进行归并,由于子数组自身有序,因此两个子数组指针从头开始往后移动,将两个子数组指针指向值的更小值放入排序数组,同时该指针向后移动,直到两个指针都达到数组尾部,将得到的排序数组作为返回值返回,作为上一层递归的子数组。
  • 递归执行第二步,直到整个数组有序

归并排序的思想还是比较直观的,归并排序本身还分为自顶向下和自底向上,我们刚刚说的这种是自顶向下的思想。而自底向上的思路也是比较直观的,会在后面给出的代码中给出详细的注释,这里就不再赘述啦,思想上都是差不多的

public class Solution {
    // 自顶向下的归并排序
    public int[] sortArray(int[] nums) {
        int len = nums.length;

        //temp是辅助数组,用于存放两个子数组中的值
        int[] temp = new int[len];
        mergeSort(nums, 0, len - 1, temp);
        return nums;
    }


    private void mergeSort(int[] nums, int left, int right, int[] temp) {
        //此时子数组长度为一,划分子数组过程结束,
        //返回进入后续归并过程
        if(right==left)
            return ;

        //将原数组划分为两个等长子数组
        int mid = left + (right - left) / 2;

        //对左侧进行归并排序
        mergeSort(nums, left, mid, temp);
        //对右侧进行归并排序
        mergeSort(nums, mid + 1, right, temp);

        // 左侧区间最后一个元素下标为mid
        // 右侧区间第一个元素下标为mid+1
        // 左右数组都有序
        // 因此nums[mid] <= nums[mid + 1]
        // 说明left到right区间内完全有序
        // 直接返回无需再次排序
        if (nums[mid] <= nums[mid + 1]) {
            return;
        }

        //对两个子数组进行排序
        mergeOfTwoSortedArray(nums, left, mid, right, temp);
    }


    private void mergeOfTwoSortedArray(int[] nums, int left, int mid, int right, int[] temp) {

        //将元素拷贝到temp中,
        System.arraycopy(nums, left, temp, left, right + 1 - left);

        // i为左侧区间指针
        int i = left;
        // j为右侧区间指针
        int j = mid + 1;

        for (int k = left; k <= right; k++) {

            
            if (i == mid + 1) {
                //左侧区间到头了
                // 直接取右侧区间元素
                nums[k] = temp[j];
                j++;
            } else if (j == right + 1) {
                // 右侧区间到头了
                // 直接去左侧区间元素
                nums[k] = temp[i];
                i++;
            } else if (temp[i] <= temp[j]) {
                // 注意写成 < 就丢失了稳定性(相同元素原来靠前的排序以后依然靠前)
                nums[k] = temp[i];
                i++;
            } else {
                // 这部分的条件等价于temp[i] > temp[j]
                nums[k] = temp[j];
                j++;
            }
        }
    }
}

时间复杂度O(nlogn)

空间复杂度O(n)

稳定的排序算法

自底向上的归并排序代码

class Solution {
    //自底向上的归并排序
    public int[] sortArray(int[] nums) {
        //temp为辅助数组
        int[] temp = new int[nums.length];
        mergeSort(nums,0,nums.length-1,temp);
        return nums;
    }

    //进行自底向上的子数组划分
    public void mergeSort(int[] nums,int left ,int right , int[] temp){
        int length = nums.length;

        // i为当前一步的子数组长度
        // 初始为1,每次翻倍
        for( int i = 1 ; i < length ;i = i*2 ){
            // j为划分子数组的左指针,
            // 每次对两个数组进行归并
            // 每次自增2*i
            for( int j = 0 ; j< length - i ; j = j + i + i ){

                //对子数组进行归并
                //j为当前子数组左侧下标
                //j+i-1是中间指针
                //为了确保右指针不越界
                //Math.min(length-1, j+i+i-1)表示右指针
                merge(nums,j,j+i-1 , Math.min(length-1, j+i+i-1) , temp);
            }
        }
    }

    /**
     * 
     * @param nums 排序后的数组
     * @param left 最左侧下标
     * @param mid  中间指针
     * @param right最右侧下标
     * @param temp 辅助数组
     */
    public void merge(int[] nums ,int left ,int mid ,int right , int[] temp){
        for( int i = left ; i <=right ; i ++){
            temp[i] = nums[i];
        }
        int i = left;
        int j = mid+1;
        for( int k = left ; k<=right ; k ++){
            if( i == mid+1){
                nums[k] = temp[j++];
            }else if(j==right+1)
                nums[k] = temp[i++];
            else if ( temp[i]<= temp[j] )
                nums[k] = temp[i++];
            else
                nums[k] = temp[j++];
        }

    }
    
}

时间复杂度O(nlogn)

空间复杂度O(n)

快速排序

基本思路:找到数组中的一个值,然后确定数组中比该值更小的值有多少个,然后把比该值更小的值放在该值左侧,比该值更大的值放在右侧,然后对左右两侧重复进行上述过程

算法思想:分治思想,对已确定位置的元素左右侧进行分治

class Solution {
    //快速排序算法
    public int[] sortArray(int[] nums) {
        quickSort(nums,0,nums.length-1);
        return nums;
    }
	
    public void quickSort(int[] nums ,int left ,int right ){
        
        //确保当前子数组长度至少为2
        if( left < right){
            //进行一步随机划分
            int i = randomizedPartition(nums,left,right);
            //对左侧进行快排
            quickSort(nums ,  left , i-1);
            //对右侧进行快排
            quickSort( nums , i +1 ,right);
        }
    }

    public int randomizedPartition(int[] nums, int left ,int right){
        
        //通过随机函数找到一个随机下标对子数组进行划分
        int i = new Random().nextInt(right - left + 1 ) + left;
        //将这个随机下标放到最右侧
        swap(nums ,right,i );
        return partition(nums,left,right);
    }
    public int partition(int[] nums ,int left ,int right){
        //将最右侧的值设为pivot
        int pivot = nums[right];
        int i = left-1;
        for( int j = left ; j <right ; j ++){
            //比pivot小的值则靠左进行放置
            if(nums[j] <= pivot){
                i++;
                swap(nums,i,j);
            }
        }
        
        //把pivot放到正确的下标i+1上
        swap(nums,i+1,right);
        //返回pivot正确下标
        return i+1;
    }

    public void swap(int[] nums ,int i ,int j ){
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

平均时间复杂度O(nlogn) ,最差时间复杂度O(n*n),最差时间复杂的情况会出现在数组原本就是降序排序,我们需要得到一个升序数组的时候,我们每次都要把最右侧的值放回靠左的位置,一共会进行n趟,每次遍历n个元素。为了减少这种情况的发生,我们加入了一个随机函数,可以尽量避免这种最差情况的发生。

空间复杂度O(nlogn)

堆排序

堆排序算是这三种里面最难理解的一种排序算法吧,这里给大家推荐一个网站www.cs.usfca.edu/~galles/vis… 这个网站上有很多算法的可视化,大家可以自己去看一下 这里简单介绍一下堆排序的思想,首先将数组模拟成一个二叉堆,我们第一次遍历的时候从堆底开始,将更大的元素逐层向上交换,最后将最大元素放在堆顶,然后我们把对顶元素与最下一层最右侧的元素进行交换,并将最大元素从堆中删除。然后由于第一次遍历的时候,相对较大的元素都在相对靠上的位置,我们将堆顶元素向下沉,结束之后,把堆顶元素与最后一个元素进行交换,并把最大元素删除,重复以上过程直到堆为空即排序结束

public class Solution {

public int[] sortArray(int[] nums) {
    int len = nums.length;
    // 将数组整理成堆
    heapify(nums);

    // 循环不变量:区间 [0, i] 堆有序
    for (int i = len - 1; i >= 1; ) {
        // 把堆顶元素(当前最大)交换到数组末尾
        swap(nums, 0, i);
        // 逐步减少堆有序的部分
        i--;
        // 下标 0 位置下沉操作,使得区间 [0, i] 堆有序
        siftDown(nums, 0, i);
    }
    return nums;
}


//将数组整理成堆(堆有序)
private void heapify(int[] nums) {
    int len = nums.length;
    // 只需要从 i = (len - 1) / 2 这个位置开始逐层下移
    for (int i = (len - 1) / 2; i >= 0; i--) {
        siftDown(nums, i, len - 1);
    }
}

/**
 * @param nums
 * @param k    当前下沉元素的下标
 * @param end  [0, end] 是 nums 的有效部分
 */
private void siftDown(int[] nums, int k, int end) {
    while (2 * k + 1 <= end) {
        int j = 2 * k + 1;
        if (j + 1 <= end && nums[j + 1] > nums[j]) {
            j++;
        }
        if (nums[j] > nums[k]) {
            swap(nums, j, k);
        } else {
            break;
        }
        k = j;
    }
}

private void swap(int[] nums, int index1, int index2) {
    int temp = nums[index1];
    nums[index1] = nums[index2];
    nums[index2] = temp;
}

时间复杂度O(nlogn)

空间复杂度O(1)

欢迎大家关注我的微信公众号:码外狂徒,记录张三与后端的爱恨情仇,每周更新两篇后端知识和至少三篇LeetCode解题思路