每日一题:LeetCode747:简单题也有大奥秘:十大排序算法扩展

104 阅读8分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路

前言

  • 大家好,我是新人掘金博主:「掘金

    • 正在坚持每日更新LeetCode每日一题,发布的题解有些会参考其他大佬的思路(参考资料的链接会放在最下面),欢迎大家关注我 ~ ~ ~
    • 今天是坚持写题解的17天(haha,从21年圣诞节开始的),大家一起加油!
  • 每日一题:LeetCode:747.至少是其他数字两倍的最大数

    • 时间:2022-01-13
    • 力扣难度:Easy
    • 个人难度:Easy
    • 数据结构:数组
    • 算法:模拟、排序

2022-01-13:LeetCode:747.至少是其他数字两倍的最大数

1. 题目描述

  • 题目:原题链接

    • 给你一个整数数组 nums ,其中总是存在 唯一的 一个最大整
    • 请你找出数组中的最大元素并检查它是否 至少是数组中每个其他数字的两倍。
    • 如果是,则返回 最大元素的下标 ,否则返回 -1
  • 输入输出规范

    • 输入:整数数组nums
    • 输出:最大数的索引
  • 输入输出示例

    • 输入:[3,6,1,0]
    • 输出:1

2. 方法:简单模拟

  • 思路

    • 本题要求数组中的最大值是否比任意一个元素的两倍都要大,最直接的思路就是一次遍历找到最大值,再一次遍历比较是否比两倍还大
    • 实际上,只要最大数比次大数的两倍大,就一定比其他元素的两倍大,因此可以优化成一次遍历
    • 值得注意的是:本题输入的数组只有一个元素时,默认返回0即可,即满足最大数比其他的两倍大
  • 复杂度分析:n 是数组的长度

    • 时间复杂度:O(n)O(n)
    • 空间复杂度:O(1)O(1)
  • 题解:直接模拟

    public int dominantIndex(int[] nums) {
        if(nums == null || nums.length == 0) return -1;
        int n = nums.length;
        if(n == 1) return 0;
        int max = nums[0];
        int secondMax = Integer.MIN_VALUE;
        int index = 0;
        for(int i = 1; i < n; i++) {
            if(nums[i] >= max){
                secondMax = max;
                max = nums[i];
                index = i;
            }else if(nums[i] >= secondMax) secondMax = nums[i];
        }
        // System.out.print(1 >= 0 << 1);
        return max >= secondMax << 1 ? index : -1;
    }
    

3. 扩展:排序算法

  • 本题非常简单,因此我们可以扩展一下程序员基本技能:排序算法

    • 十大排序算法:冒泡、快排、插入、选择、归并排序堆排序、希尔排序、基数排序、计数排序桶排序
    • 排序的对象:数组、链表等

十大排序算法.png

  • 冒泡 & 选择 & 插入

    • 作为最简单的三种排序(都属于比较排序),这里简单讲一下三者的特点与区别,以升序为例
    • 冒泡排序:进行 n 轮,每轮逐个对比元素大小,将大的元素后移,即每一轮会将最大元素移到数组末尾,同时数组局部也发生排序
    • 选择排序:进行 n 轮,每轮通过比较,寻找最大的元素,记录其索引,将其与末尾元素交换,同样是每一轮会将最大元素移到数组末尾,但是数组局部不会发生改变
    • 插入排序:进行 n 轮,每轮取出未排序的一个元素(按顺序),然后比较其与前面以及排好序的元素,找到其要插入的位置并插入,不会取找到最大最小元素,数组局部不会发生改变
    • 此外,本文主要介绍最常见、使用最广泛的几种排序:快排、归并、堆排序
  • 快速排序:QuickSort:partition & sort

    • 思想:分治

      • 提前设置一个基准,在每轮排序时通过该基准将序列分成两部分,大于基准的和小于基准的各自在一边
      • 以此达到二分的效果,最终分成的多段序列都各自有序
      • 基准可以取序列中任意一个元素,如开头、中间、结尾
    • 复杂度

      • 时间复杂度:O(nlogn)O(nlogn),最坏情况下O(n2)O(n^2)
      • 空间复杂度:O(logn)O(logn),主要是递归函数的栈空间
      • 排序不稳定,性能受输入数据的影响,因此需要优化
    • 快排优化:随机乱序避免二分失效

      • 实际上,由于快排对序列的分布要求较高,即最坏情况下会退化回O(n2)O(n^2)的时间复杂度
      • 所以针对难搞的数据集,要先对数据随机乱序,再取基准进行快排。
    • 源码:取开头为基准,双向遍历

      public class QuickSorter {
          private static Random random = new Random();
      
          public static void main(String[] args) throws IOException {
              BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
              String[] strings = br.readLine().split(" ");
              int[] nums = new int[strings.length];
              for (int i = 0; i < strings.length; i++) {
                  nums[i] = Integer.parseInt(strings[i]);
              }
              new QuickSorter().quickSort(nums, 0, nums.length - 1);
              System.out.println(Arrays.toString(nums));
          }
      
          private void quickSort(int[] nums, int l, int r) {
              if (l < r) {
                  int idx = partition(nums, l, r);
                  quickSort(nums, l, idx - 1);
                  quickSort(nums, idx + 1, r);
              }
          }
      
          private int partition(int[] nums, int l, int r) {
              int idx = random.nextInt(r - l) + l;
              swap(nums, l, idx);
              int pivot = nums[l];
              while (l < r) {
                  while (l < r && nums[r] >= pivot) --r;
                  nums[l] = nums[r];
                  while (l < r && nums[l] <= pivot) ++l;
                  nums[r] = nums[l];
              }
              nums[l] = pivot; // 注意交换基准元素和边界元素
              return l;
          }
      
          private void swap(int[] nums, int i, int j) {
              // 位运算
              nums[i] = nums[i] ^ nums[j] ^ (nums[j] = nums[i]);
              // int temp = nums[i];
              // nums[i] = nums[j];
              // nums[j] = temp;
          }
      }
      
    • 源码:取开头为基准,单向遍历

      private int partition(int[] nums, int l, int r) {
          int idx = random.nextInt(r - l) + l;
          swap(nums, l, idx);
          int pivot = nums[l];
          int mark = l;
          for (int i = l + 1; i <= r; i++) {
              if(nums[i] < pivot) {
                  mark++;
                  swap(nums, mark, i);
              }
          }
          swap(nums, l , mark); // 注意交换基准元素和边界元素
          return mark;
      }
      
  • 归并排序:MergeSort:divide & merge

    • 思想:分治多路归并(多叉树)

      • 归并排序也是通过分治的思想,将序列不断分解为多个子序列,然后再进行排序,各个子序列有序后,合并得到的就是有序序列
      • 一般的情况下都是用二路归并,即类似二分的方式从序列中点来分解、合并序列,而一些大文件排序的场景,可能会用到多路归并
      • 与快排不同的是,快排在按照基准partition分区的过程中,也会进行一定的排序(大的一边、小的一边),而归并排序是完全将序列划分到不可分时才进行排序
    • 复杂度分析

      • 时间复杂度:O(nlogn)O(nlogn),合并的复杂度为O(n)O(n),而分治的过程是一个完全二叉树,深度为O(logn)O(logn),两者嵌套,总的复杂度O(nlogn)O(nlogn)
      • 空间复杂度:O(n)O(n),需要一个辅助数组
      • 排序稳定,性能不受输入数据的影响
    • 源码

      public class MergeSorterArray {
      
          public static void main(String[] args) throws IOException {
              BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
              String[] strings = br.readLine().split(" ");
              int[] nums = new int[strings.length];
              for (int i = 0; i < strings.length; i++) {
                  nums[i] = Integer.parseInt(strings[i]); // 7354613
              }
              new MergeSorterArray().mergeSort(nums, 0, nums.length - 1, new int[nums.length]);
              System.out.println(Arrays.toString(nums));
          }
      
      
          public void mergeSort(int[] nums, int left, int right, int[] save) {
              if (left < right) {
                  int mid = (right - left) / 2 + left;
                  mergeSort(nums, left, mid, save);
                  mergeSort(nums, mid + 1, right, save);
                  merge(nums, left, mid, right, save);
              }
          }
      
          private void merge(int[] nums, int left, int mid, int right, int[] save) {
              int i = left, j = mid + 1; // 双指针,用来合并数组
              int index = 0;
              while (i <= mid && j <= right) {
                  if (nums[i] < nums[j]) {
                      save[index++] = nums[i++];
                  } else {
                      save[index++] = nums[j++];
                  }
              }
              while (i <= mid) {
                  save[index++] = nums[i++];
              }
              while (j <= right) {
                  save[index++] = nums[j++];
              }
              // 将排序后的辅助数组copy回原数组
              index = 0;
              while (left <= right) {
                  nums[left++] = save[index++];
              }
          }
      }
      
  • 堆排序:HeapSort

    • 思想:最小堆的构建与调整

      • 堆排序是一种选择排序,是利用完全二叉树的性质来优化排序的一种算法
      • 主要分为构建初始堆交换堆顶和末尾元素重建堆三个方面
      • 其中,要注意完全二叉树的叶子节点的特性,以及重建堆的时候有一半的树无需操作
      • 值得注意的是,堆排序无需真的实现树结构,只需要用数组模拟小顶堆即可
    • 复杂度分析

      • 时间复杂度:O(nlogn)O(nlogn),构建初始堆的复杂度为O(n)O(n),而重建堆的复杂度为O(nlogn)O(nlogn),两者平行,总的复杂度O(nlogn)O(nlogn)
      • 空间复杂度:O(1)O(1),无需额外空间
      • 排序不稳定,性能不受输入数据的影响
    • 源码

      public class HeapSorter {
      
          public static void main(String[] args) throws IOException {
              BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
              String[] strings = br.readLine().split(" ");
              int[] nums = new int[strings.length];
              for (int i = 0; i < strings.length; i++) {
                  nums[i] = Integer.parseInt(strings[i]); // 7354613
              }
              new HeapSorter().heapSort(nums, nums.length);
              System.out.println(Arrays.toString(nums));
          }
      
          public void heapSort(int[] nums, int n) {
              buildHeap(nums, n);
              for (int i = n - 1; i >= 0; i--) {
                  swap(nums, i, 0); // 交换堆顶和末尾的元素,最大值
                  heapify(nums, i, 0); // 从根节点开始调整整个树
              }
          }
      
          // 创建大顶堆
          private void buildHeap(int[] nums, int n) {
              int lastIndex = n - 1;
              int parentIndex = (lastIndex - 1) / 2;
              for (int i = parentIndex; i >= 0; i--) {
                  heapify(nums, n, i);
              }
              System.out.println(Arrays.toString(nums));
          }
      
          private void heapify(int[] nums, int n, int i) {
              if (i >= n) return;
              int left = 2 * i + 1, right = 2 * i + 2; // 左子节点和后子节点的索引
              int max = i; // 最大元素的索引,初始化为目前要调整的子树的根节点
              // 比较交换局部最大值
              if (left < n && nums[left] > nums[max]) max = left;
              if (right < n && nums[right] > nums[max]) max = right;
              if (max != i) { // 如果max变化了
                  swap(nums, i, max);
                  heapify(nums, n, max); // 继续取调整节点值被提升到 i 节点后,新的需要调整的子树
              }
          }
      
          private void swap(int[] nums, int i, int j) {
              int temp = nums[i];
              nums[i] = nums[j];
              nums[j] = temp;
          }
      }
      

趁热打铁:面试高频


最后

如果本文有所帮助的话,欢迎大家可以给个三连「点赞」&「收藏」&「关注」 ~ ~ ~ 也希望大家有空的时候光临我的其他平台,上面会更新Java面经、八股文、刷题记录等等,欢迎大家光临交流,谢谢!