每日算法总结——快速排序、堆排序

一、快速排序

荷兰国旗问题
问题一

给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( N ) O(N) O(N)

【解题思路】:准备一个指针i和一个指针PP用于划分小于等于区域,从数组的首部开始,将当前数arr[i]num作比较:

  1. arr[i] <= num,则arr[i]和小于等于区域的下一个数arr[P+1]交换,小于等于区域右扩,即P++,同时i++
  2. arr[i] > num,则i++
问题二 (荷兰国旗问题)

给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( N ) O(N) O(N)

【解题思路】:准备三个指针i,p1,p2p1用于划分小于区域,p2用于划分大于区域,[p1, p2]为等于区域,从数组的首部开始,将当前数arr[i]num作比较:

  1. arr[i] < num,则arr[i]和小于区域的下一个数交换,小于区域向右扩,i++
  2. arr[i] == num,则i++
  3. arr[i] > num,则arr[i]和大于区域前一个数交换,大于区域左扩,i不变,因为交换过来的数还没有作判断。

快速排序和这两个问题的思想类似,核心思想都是大于/小于区域的划分

快速排序算法思想
  1. 从数列中随机挑出一个元素,称为 “基准”(pivot)
  2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以放中间)。在这个分区结束之后,与该基准相同的数就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
Java实现
public class QuickSort {
    /**
     * 快速排序算法思想
     * 1. 从数列中随机挑出一个元素,称为"基准"(pivot)
     * 2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以放中间)。在这个分区结束之后,与该基准相同的数就处于数列的中间位置。这个称为分区(partition)操作。
     * 3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
     */
    public static void quickSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        quickSort(arr, 0, arr.length - 1);
    }

    // arr[l~r]排好序
    public static void quickSort(int[] arr, int l, int r) {
        if (l < r) {
            // 首先等概率随机选一个数,将其与最右位置的数交换。
            swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
            int[] p = partition(arr, l, r);
            quickSort(arr, l, p[0] - 1);
            quickSort(arr, p[0] + 1, r);
        }
    }

    /**
     * 处理arr[l~r]的函数
     * 默认以arr[r]作划分,arr[r] -> p : <p, =p, >p
     * 返回等于区域[左边界,右边界],所以返回一个长度为2的数组res, res[0] res[1]
     */
    public static int[] partition(int[] arr, int l, int r) {
        int less = l, more = r - 1;
        while (l <= more) {
            if (arr[l] < arr[r]) {
                swap(arr, l++, less++);
            } else if (arr[l] > arr[r]) {
                swap(arr, l, more--);
            } else {
                l++;
            }
        }
        swap(arr, r, --more);
        return new int[]{less, more};
    }

    public static void swap(int[] arr, int src, int dest) {
        int temp = arr[src];
        arr[src] = arr[dest];
        arr[dest] = temp;
    }
}

复杂度分析:由于基准值是随机选择的,所以有可能选的基准值就是真正的中值,此时算法复杂度最低;但也有可能选到边界值,此时算法复杂度最差(接近冒泡排序);所以综合考虑全部可能性,并利用数学方法求期望可得平均复杂度为 O ( N ∗ l o g   N ) O(N*log\ N) O(Nlog N)

实战:颜色分类

二、堆排序

堆结构

堆是一种特殊的完全二叉树(complete binary tree)。完全二叉树的一个 “优秀” 的性质是,除了最底层之外,每一层都是满的,最底层所有的数都靠左,这使得堆可以利用数组来表示(普通的一般的二叉树通常用链表作为基本容器表示),每一个结点对应数组中的一个元素。

对于给定的某个结点的下标 i,可以很容易的计算出这个结点的父结点、孩子结点的下标:

  • Parent(i) = floor(i - 1 / 2)i 的父节点下标
  • Left(i) = 2i + 1i 的左子节点下标
  • Right(i) = 2i + 2i 的右子节点下标

二叉堆一般分为两种:最大堆和最小堆

  • 最大堆:
    最大堆中的最大元素值出现在根结点(堆顶)
    堆中每个父节点的元素值都大于等于其孩子结点(如果存在)
  • 最小堆:
    最小堆中的最小元素值出现在根结点(堆顶)
    堆中每个父节点的元素值都小于等于其孩子结点(如果存在)

堆的核心操作有两个:heapify(能否向下移动)heapInsert(能否向上移动)

堆的建立和维护

考虑两个问题:

  1. 删除堆顶元素后,如何调整数组成为新堆?

    最后一个元素(代号A)移动到根元素的位置,此时A一定小于其某个子节点,因此需要执行heapify向下移动

  2. 插入堆顶元素后,如何调整数组成为新堆?

    把新元素放在末尾,然后和其父节点做比较,执行heapInsert向上移动。

堆如何建立?

可以将第一个元素看作一个堆,然后不断向其中添加新元素,并不断进行heapInsert。

堆排序算法描述

对数组建立最大堆,然后将堆最后一个位置的值与根节点的值交换,此时在最后一个位置输出最大值,即调整堆的大小使其减一,并对根节点执行heapify操作,对调整后的堆重复上述操作。
由于每次输出的最大元素会腾出第一个空间,因此,恰好可以放置这样的元素而不需要额外空间。

public class HeapSort {
    /**
     * 堆排序算法描述
     * 对数组建立最大堆,然后将堆最后一个位置的值与根节点的值交换,此时在最后一个位置输出最大值,即调整堆的大小使其减一,并对根节点执行heapify操作,对调整后的堆重复上述操作。
     */
    public static void heapSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        // 建立大根堆 O(N)
        for (int i = 1; i < arr.length; i++) {
            // O(log N)
            heapInsert(arr, i);
        }
        // // 建立大根堆更快一点的方法 O(N)
        // for (int i = arr.length - 1; i >= 0; i--) {
        //     heapify(arr, i, arr.length);
        // }
        // 输出排序后的数组
        int heapSize = arr.length;
        // O(N)
        while (heapSize > 0) {
            // O(1)
            swap(arr, 0, --heapSize);
            // O(logN)
            heapify(arr, 0, heapSize);
        }
    }

    /**
     * 某个数现在处于index位置,能否继续向上移动
     *
     * @param arr   堆
     * @param index 该数在arr中的下标
     */
    public static void heapInsert(int[] arr, int index) {
        while (arr[index] > arr[(index - 1) / 2]) {
            swap(arr, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    /**
     * 某个数在index位置,能否向下移动
     *
     * @param arr      堆
     * @param index    该数在arr中的下标
     * @param heapSize 堆的大小,用于判断是否有左/右孩子
     */
    public static void heapify(int[] arr, int index, int heapSize) {
        // 左孩子的下标
        int left = 2 * index + 1;
        // 是否还有孩子
        while (left < heapSize) {
        	// 左孩子与右孩子比较
            int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
            // 大孩子与父亲比较
            largest = arr[largest] > arr[index] ? largest : index;
            if (largest == index) {
                return;
            }
            swap(arr, largest, index);
            index = largest;
            left = 2 * index + 1;
        }
    }

    public static void swap(int[] arr, int src, int dest) {
        int temp = arr[src];
        arr[src] = arr[dest];
        arr[dest] = temp;
    }
}

为什么上述代码中第二种建立大根堆的过程,时间复杂度可以为 O ( N ) O(N) O(N)呢?

  • 由二叉树叶节点开始逐步对每个节点执行heapify过程也是可以将数组大根堆化,但其实对于叶节点heapify不进行任何操作,因为无法再向下移动了,所以heapify执行的次数实际只有 N 2 \frac{N}{2} 2N次(即从非叶节点处),将这 N 2 \frac{N}{2} 2N个节点执行heapify的复杂度累加起来就是 O ( N ) O(N) O(N)

但推排序总体的复杂度不变,仍是 O ( N l o g N ) O(NlogN) O(NlogN)

实战:数组中的第K个最大元素
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值