算法通关村第10关——快速排序并不难(青铜)

算法通关村第10关——快速排序并不难(青铜)

前言

什么是快速排序?

快速排序是一种常用的排序算法,也是基于比较的排序算法。它通过分治的思想将一个大问题转化为多个小问题来解决。

实现快速排序的核心思想是选取一个基准元素(通常选取第一个元素),将数组分成两部分,一部分是小于基准元素的,另一部分是大于基准元素的。然后对这两部分分别进行递归快速排序,最终得到有序的结果。

具体实现步骤如下:

  1. 选取基准元素。
  2. 将数组中小于基准元素的元素移到基准元素左边,大于基准元素的元素移到基准元素右边。
  3. 对基准元素左边和右边的子数组分别进行递归快速排序。

快速排序适合解决需要排序的问题。它的时间复杂度为平均情况下的O(nlogn),最坏情况下的时间复杂度为O(n^2)。由于快速排序使用了递归,所以在处理大规模的数据时可能会出现栈溢出的问题,可以通过优化算法或者使用尾递归来解决。

简单的例子讲解

假设有一个数组[5, 9, 3, 7, 2],使用快速排序对其进行排序。

  1. 选取第一个元素5作为基准。
  2. 将小于5的元素移到基准的左边,大于5的元素移到基准的右边。此时数组变为[3, 2, 5, 9, 7]。
  3. 对基准元素左边的子数组[3, 2]进行递归快速排序,选取基准元素3。
  4. 将小于3的元素移到基准的左边,大于3的元素移到基准的右边。此时数组变为[2, 3]。
  5. 对基准元素右边的子数组[9, 7]进行递归快速排序,选取基准元素9。
  6. 将小于9的元素移到基准的左边,大于9的元素移到基准的右边。此时数组变为[7, 9]。
  7. 最终得到有序数组[2, 3, 5, 7, 9]。

这个例子展示了快速排序在每一轮选择基准元素、划分子数组和递归排序的过程。通过多次划分和排序,最终可以得到整个数组的有序结果。

补充:

常用的排序算法有以下几种:

  1. 冒泡排序(Bubble Sort):通过不断交换相邻元素使得最大(或最小)元素逐渐“冒泡”到数组的末尾(或开头)。时间复杂度为O(n^2),适用于小规模数据。
  2. 选择排序(Selection Sort):每次从待排序的数据中选择最小(或最大)的元素,放到已排序部分的末尾。时间复杂度为O(n^2),适用于小规模数据。
  3. 插入排序(Insertion Sort):将一个待排序的元素插入到已排好序的部分中的合适位置,使得已排序部分始终保持有序。时间复杂度为O(n^2),适用于基本有序的数据。
  4. 快速排序(Quick Sort):采用分治的思想,选择一个基准元素,将小于它的元素放在左边,大于它的元素放在右边,然后对左右两个子序列递归地进行快速排序。时间复杂度为O(nlogn),适用于大规模数据。
  5. 归并排序(Merge Sort):采用分治的思想,将待排序的序列分成两个子序列,然后分别对两个子序列进行排序,最后将两个有序的子序列合并成一个有序序列。时间复杂度为O(nlogn),适用于大规模数据。
  6. 堆排序(Heap Sort):将待排序的序列构建成一个堆,然后将堆顶元素与堆的最后一个元素交换,并调整堆结构,重复此过程直到整个序列有序。时间复杂度为O(nlogn),适用于大规模数据。

1. 快速排序的基本过程

快速排序的核心框架是**“二叉树的前序遍历+对撞型双指针”**。我们在《一维数组》一章提到过”双指针思路“:在处理奇偶等情况时会使用两个游标,一个从前向后,一个是从后向前来比较,根据结果来决定继续移动还是停止等待。快速排序的每一轮进行的时候都是类似的双指针策略,而递归的过程本质上就是二叉树的前序递归调用。

快速排序的核心框架有很多种方式实现,下面这个就是采用对撞型双指针的方式来进行元素的交换和移动。

void quickSort(int[] array, int start, int end) {
    // 如果start >= end,表示当前子序列只有一个或没有元素,不需要排序,直接返回
    if (start >= end) {
        return;
    }
    
    // 这里就是一个对撞的双指针操作
    int left = start, right = end;
    // 选择基准元素为中间位置的元素
    int pivot = array[(start + end) / 2];
    
    while (left <= right) {
        // 左指针向右移动,找到第一个大于等于基准元素的值
        while (left <= right && array[left] < pivot) {
            left++;
        }
        
        // 右指针向左移动,找到第一个小于等于基准元素的值
        while (left <= right && array[right] > pivot) {
            right--;
        }
        
        // 如果左指针小于等于右指针,则交换两个指针所指的元素,并更新指针位置
        if (left <= right) {
            int temp = array[left];
            array[left] = array[right];
            array[right] = temp;
            left++;
            right--;
        }
    }
    
    // 先处理元素再分别递归处理两侧分支,与二叉树的前序遍历非常像
    quickSort(array, start, right);
    quickSort(array, left, end); 
}

2.一道例题讲解

下一步通过一个leetcode题来感受一下:
leetcode 912. 排序数组

class Solution {
    public int[] sortArray(int[] nums) {
        quickSort(nums, 0, nums.length - 1);
        return nums;
    }

    private void quickSort(int[] nums, int start, int end) {
        if (start >= end) {
            return;
        }

        int left = start;
        int right = end;
        int pivot = nums[(start + end) / 2];

        while (left <= right) {
            while (left <= right && nums[left] < pivot) {
                left++;
            }

            while (left <= right && nums[right] > pivot) {
                right--;
            }

            if (left <= right) {
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
                left++;
                right--;
            }
        }

        quickSort(nums, start, right);
        quickSort(nums, left, end);
    }
}

这道题非常经典,还可以使用各自算法来实现,也是一道练习八大算法的题目,不过使用leetcode很多算法可能会超时,例如:冒泡

所以这道题还可以用,堆和归并来实现。

复杂度分析

快速排序的时间复杂度计算比较麻烦一些。从原理来看,如果我们选择的pivot每次都正好在中间,效率是最高的,但是这是无法保证的,因此我们需要从最好、最坏和中间情况来分析

最坏情况就是如果每次选择的恰好都是low结点作为pivot,如果元素恰好都是逆序的,此时时间复杂度为O(n^2)

如果元素恰好都是有序的,则时间复杂度为O(n^2)

折中的情况是每次选择的都是中间结点,此时序列每次都是长度相等的序列,此时的时间复杂度为(O(nlogn))

补充:最大堆

最大堆是一种特殊的二叉树数据结构,满足以下性质:

  1. 所有父节点的值都大于或等于其子节点的值。
  2. 根节点的值是堆中的最大元素。

最大堆常用数组来表示,其中根节点存储在索引位置1上,而对于任意节点i,其左子节点存储在索引位置2i上,右子节点存储在索引位置2i+1上。

最大堆主要用于实现优先队列、堆排序等算法。通过保持最大堆的性质,可以快速找到堆中的最大元素,并能够高效地插入和删除元素。

当数组表示的最大堆为 [25, 17, 12, 10, 6, 9, 5],对应的二叉树如下所示:

        25
      /    \
    17      12
   /  \    / 
 10    6  9  
/
5

注意:该图是一种常见的表示方式,但实际上最大堆在内存中并不以这种形式存在,而是通过数组来表示。

所以构建最大堆的算法如下:

private void buildMaxHeap(int[] nums) {
    // 从最后一个非叶子节点开始,依次向上调整堆
    for (int i = (nums.length - 1) / 2; i >= 0; i--) {
        adjustHeap(nums, i, nums.length);
    }
}

private void adjustHeap(int[] nums, int parent, int len) {
    int temp = nums[parent];
    int child = 2 * parent + 1; // 左孩子节点
    
    while (child < len) {
        // 如果存在右孩子节点并且右孩子比左孩子大,则选择右孩子作为交换对象
        if (child + 1 < len && nums[child] < nums[child + 1]) {
            child++;
        }
        
        // 如果父节点比子节点大,则无需调整,直接退出循环
        if (temp >= nums[child]) {
            break;
        }
        
        // 将子节点的值赋给父节点
        nums[parent] = nums[child];
        
        // 进入下一层调整
        parent = child;
        child = 2 * parent + 1;
    }
    
    // 将原父节点的值放入最终位置
    nums[parent] = temp;
}

如果使用堆排序实现对整数数组进行排序,并且按照升序排列,也就是上面那一题,代码如下:

class Solution {
    public int[] sortArray(int[] nums) {
        // 构建最大堆
        buildMaxHeap(nums);
        
        // 循环将堆顶元素与末尾元素交换,并重新调整堆
        for (int i = nums.length - 1; i > 0; i--) {
            swap(nums, 0, i);
            adjustHeap(nums, 0, i);
        }
        
        return nums;
    }
    
    private void buildMaxHeap(int[] nums) {
        // 从最后一个非叶子节点开始,依次向上调整堆
        for (int i = (nums.length - 1) / 2; i >= 0; i--) {
            adjustHeap(nums, i, nums.length);
        }
    }
    
    private void adjustHeap(int[] nums, int parent, int len) {
        int temp = nums[parent];
        int child = 2 * parent + 1; // 左孩子节点
        
        while (child < len) {
            // 如果存在右孩子节点并且右孩子比左孩子大,则选择右孩子作为交换对象
            if (child + 1 < len && nums[child] < nums[child + 1]) {
                child++;
            }
            
            // 如果父节点比子节点大,则无需调整,直接退出循环
            if (temp >= nums[child]) {
                break;
            }
            
            // 将子节点的值赋给父节点
            nums[parent] = nums[child];
            
            // 进入下一层调整
            parent = child;
            child = 2 * parent + 1;
        }
        
        // 将原父节点的值放入最终位置
        nums[parent] = temp;
    }
    
    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

over~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值