力扣hot100道【贪心算法后续解题方法心得】(三)

news2024/12/26 18:36:53

力扣hot100道【贪心算法后续解题方法心得】

  • 十四、贪心算法
    • 关键解题思路
    • 1、买卖股票的最佳时机
    • 2、跳跃游戏
    • 3、跳跃游戏 | |
    • 4、划分字母区间
  • 十五、动态规划
    • 什么是动态规划?
    • 关键解题思路和步骤
    • 1、打家劫舍
    • 2、01背包问题
    • 3、完全平方式
    • 4、零钱兑换
    • 5、单词拆分
    • 6、最长递增子序列
    • 7、乘积最大子序列
    • 8、分割等和子集
  • 十六、多为动态规划
    • 1、不同路径
    • 2、最小路径和
    • 3、最长回文子串
    • 4、最长公共子序列
    • 5、编辑距离
  • 十七、技巧
  • 十八、排序算法
    • 概述
    • 1、冒泡排序(Bubble Sort)
    • 2、选择排序(Selection Sort)
    • 3、插入排序
    • 4、快速排序(Quick Sort)!!!
    • 5、归并排序(Merge Sort)
    • 6、堆排序(Heap Sort)
    • 7、希尔排序(Shell Sort)
    • 8、桶排序(Bucket Sort)
    • 9、基数排序(Radix Sort)
  • 其他相关内容
    • 1. 哈希-矩阵解题心得(一):[https://blog.csdn.net/a147775/article/details/143558612](https://blog.csdn.net/a147775/article/details/143558612)
    • 2. 链表-堆解题心得(二):[https://blog.csdn.net/a147775/article/details/144218993](https://blog.csdn.net/a147775/article/details/144218993)

十四、贪心算法

关键解题思路

理解贪心的本质是:选择每一阶段的局部最优,从而达到全局最优

贪心算法一般分为如下四步:

  • 将问题分解为若干个子问题
  • 找出适合的贪心策略
  • 求解每一个子问题的最优解
  • 将局部最优解堆叠成全局最优解

1、买卖股票的最佳时机

1)解题关键思路

  • 在最低价格买入,最高利润卖出(思路是错误的) 如:[2,100,1,5]可能在最低价格1买入,然后5卖出吗,不可能。
  • 总的来说应该是在遍历每一天中后续相邻天数的净利润相加,净利润<0舍弃重新开始得到最大值。

2)代码

 public int maxProfit(int[] prices) {
        int maxSum = 0;
        int sum = 0;
        for (int i = 0; i < prices.length; i++) {
            sum += prices[i];
            if (sum < 0) sum = 0;
            maxSum = Math.max(maxSum, sum);
        }
        return maxSum;
    }

3)分析

  • 求每一天的净利润相加,得到的就是最大值,<0就直接舍弃从下一个元素开始继续重复上述操作。

2、跳跃游戏

1)解题思路与关键难点

  • 就是跳到哪里进行覆盖,如果覆盖是数组长度索引最大,直接可以

关键难点
知道思路是什么,但却写不出代码,不知道应该怎么写代码
解答

  • 每次移动只能在 cover 的范围内移动,每移动一个元素,cover 得到该元素数值(新的覆盖范围)的补充,让 i 继续移动下去。
  • 贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
    1)解题关键思路
  • coverRange覆盖达到nums.length-1
  • 每次遍历最大遍历只能到达converRange
    2)代码
// 思路:求每个下标下的最大覆盖位置
    public boolean canJump(int[] nums) {
        if (nums.length ==1) return true;
        int coverRange = 0;
        // 为什么是相等?
        // 因为可以跳到该覆盖索引的下标下
        for (int i = 0; i <= coverRange; i++) {
            coverRange = Math.max(coverRange, i + nums[i]);
            if (coverRange >= nums.length -1) return true;
        }
        return false;
    }

3)分析

  • 判断coverRange覆盖达到nums.length-1返回true
  • 每次遍历最大遍历只能到达converRange,并在每次更新converRange的最大赋值。

3、跳跃游戏 | |

1)解题关键
要想清楚什么时候步数才一定要加一呢?
关键点:
所以真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!

这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖
2)代码

// 尽可能的选择跳最远的
    public int jump(int[] nums) {
        if (nums.length == 1) return 0;

        int nextCoverRange = 0;
        int currentCover = 0;
        int res = 0;
        for (int i = 0; i <= nextCoverRange ; i++) {
            nextCoverRange = Math.max(nextCoverRange , i + nums[i]);
            if (nextCoverRange >= nums.length - 1) {
                res++;
                break;
            }
            // 在当前覆盖范围内寻找最大覆盖范围,
            // 如果走完当前覆盖范围计算下一步最大的覆盖范围还没有到达终点,
            // 再加一步进行移动
            if (i == currentCover) {
            // 开始跳跃,跳跃范围是nextCoverRange 
                res++;
                currentCover = nextCoverRange ;
            }
        }
        return res;
    }

3)分析

  • 在当前覆盖索引寻找nextCoverRange 最大值是否覆盖nums.length - 1,如果覆盖直接+1退出,如果遍历结束还没有就进行跳转到下一个nextConverRange中的某个位置继续遍历。

4、划分字母区间

1)关键解题思路
题目要求同一个字母只能出现在一个片段里面,故而解题关键就是找出同一个字母出现的最远的索引即可
2)代码

public List<Integer> partitionLabels(String s) {
        // 记录每个字母最后的索引位置
        int[] last = new int[26];
        char[] chars = s.toCharArray();
        // 获取每个字母最后的起始位置
        for (int i = 0; i < chars.length; i++) {
            last[chars[i] - 'a'] = i;
        }
        List<Integer> list = new ArrayList<>();
        int end = 0;
        int start = 0;
        for (int i = 0; i < chars.length; i++) {
            // 什么时候加入list?
            end = Math.max(end, last[i]);
            if (i == end) {
                list.add(end - start + 1);
                start = end + 1;
            }
        }
        return list;
    }

3)分析

  • 最重要一步就是记录同一个字母的最后索引位置,然后遍历等到i == end时候收集一个区间的结果

十五、动态规划

什么是动态规划?

动态规划(Dynamic Programming,简称DP)是一种解决具有重叠子问题和最优子结构特性的问题的方法。其核心思想是将一个复杂的问题分解成一系列更小的子问题,并通过存储这些子问题的解来避免重复计算,从而提高算法效率,即为利用空间存储已经计算过的值,达到空间换时间。

关键解题思路和步骤

思路:
将一个复杂的问题分解成一系列更小的子问题,通过存储这些子问题的解进行后续问题的解,避免重复计算。

步骤:

  1. 确定dp数组以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

为什么是先进行递推公式的确定再进行初始化呢?

因为一些情况是递推公式决定了dp数组要如何初始化!

1、打家劫舍

1)解题关键

  1. 确定dp数组含义
  2. 根据第i家偷还是不偷,确定递推公式,如果偷:dp[i] = dp[i-2] + num[i],不偷就是:dp[i] = dp[i-1],因为是求最大,故而递推公式为:dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]);
  3. 初始化,根据递推公式可知,dp[0] = 0,dp[1] = nums[0]必偷。
  4. 确定遍历顺序,从0开始偷进行计算。
    2)代码
 public int rob(int[] nums) {
        int length = nums.length;
        if ( length == 0) return 0;
        int[] dp = new int[length + 1];
        dp[0] = 0;
        dp[1] = nums[0];
        for (int i = 2; i <= length; i++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i-1]);
        }
        return dp[length];
    }

3)分析同1)

2、01背包问题

1)解题关键
2)关键优化代码


  // 外层循环遍历每个类型的研究材料
        for (int i = 0; i < M; i++) {
            // 内层循环从 N 空间逐渐减少到当前研究材料所占空间
            for (int j = N; j >= costs[i]; j--) {
                // 考虑当前研究材料选择和不选择的情况,选择最大值
                dp[j] = Math.max(dp[j], dp[j - costs[i]] + values[i]);
            }
        }

3)分析

  • 确定遍历顺序,背包遍历顺序为倒叙,防止同一个物品被添加多次。

3、完全平方式

1)解题关键

  • 动态规划5个步骤,并类比完全背包和01背包解法。
  • 无非就是1,4,9这些是物品重量然后n是背包容量,可以重复添加,求最小次数加满背包。
  • 遍历顺序就是先遍历物品再遍历背包
  • 类比完全背包问题
    2)代码
 public int numSquares(int n) {
        // 把平方当作物品(无限使用),然后n代表背包容量
        // 刚好装满这个背包最少需要多少个物品
        // dp[i]表示装满容量为i的背包最少需要多少个物品
        int[] dp = new int[n + 1];
        // 因为求的是min,防止递推公式的值被初始值覆盖
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 1; i*i <= n; i++) { //遍历物品
            for (int j = i*i; j <= n; j++) {
                dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }

3)分析

  • 完全背包问题,无非就是一个物品可以重复添加达到背包被装满为止的最大值。

4、零钱兑换

解法同上面一样

5、单词拆分

1)关键解题思路
在这里插入图片描述
dp[i]:表示i长度的字符串能否被若干个单词组成。故而推导公式 dp[j] && wordDictSet.contains(s.substring(j, i))
-> dp[i] = true

2)代码

 // 典型背包问题 把wordDict当作物品即可
    public boolean wordBreak(String s, List<String> wordDict) {
        Set<String> wordDictSet = new HashSet(wordDict);
        int sLength = s.length();
        // 代表容量背包的最大值
        boolean[] dp = new boolean[sLength + 1];
        dp[0] = true;
        for (int i = 1; i <= s.length(); i++) {
            for (int j = 0; j < i; j++) {
                if (dp[j] && wordDictSet.contains(s.substring(j, i))) {
                    dp[i] = true;
                }
            }

        }
        return dp[sLength];
    }

6、最长递增子序列

1)关键解题步骤
dp[i]:含义代表的是对应的索引下标的最长递增子序列。
2)代码

 public int lengthOfLIS(int[] nums) {
        // 以nums[i]为结尾的最长递增子序列
        int[] dp = new int[nums.length];
        // 最低要求都是1
        Arrays.fill(dp, 1);
        int res = 1;
        for (int i = 0; i < nums.length; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            res = Math.max(res, dp[i]);
        }
        // 结果不一定是对应的结尾可能是之前的
        return res;
    }

3)分析
如果nums[i] > nums[j],代表有一个索引下标符合条件,不断遍历寻找最大值。有点类似单词拆分题目的解法

7、乘积最大子序列

1)解题关键
定义两个变量 iMax 和 iMin分别用于记录[0,i)为止的最大乘积和最小乘积。这是因为负数乘以最大值可能会变成最小值而负数乘以最小值可能会变成最大值。同时,还需要一个变量 max来记录全局的最大乘积。
2)代码

 public int maxProduct(int[] nums) {
        // 设置最大值为初始值
        int max = Integer.MIN_VALUE,imax = 1, imin = 1;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] < 0) {
                int temp = imax;
                imax = imin;
                imin = temp;
            }
            imax = Math.max(imax * nums[i], nums[i]);
            imin = Math.min(imin * nums[i], nums[i]);
            max = Math.max(imax, max);
        }
        return max;
    }

8、分割等和子集

1)关键解题思路

  • 求相同的等和,故而进行累加sum,判断sum % 2 != 0 。
  • target = sum / 2,此时可以看作是背包容量,数组里面的元素就是物品,无非就是从添加物品能否填满背包。
    2)代码
 public boolean canPartition(int[] nums) {
        int sum = 0;
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            sum += nums[i];
        }
        if (sum % 2 != 0) return false;
        int target = sum / 2;
        int[] dp = new int[target + 1];
        dp[0] = 0;
        for (int i = 1; i < n; i++) {
            for (int j = target; j >=nums[i]  ; j--) {
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        return target == dp[target];
    }

十六、多为动态规划

1、不同路径

1)解题思路
某一个网格的路径 = 左边网格右走一步 + 上边网格下走一步。
定义 dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
2)代码

//2.空间优化
    public int uniquePaths(int m, int n) {
        // dp[i][j]表示到达(i,j)的路径数
        int[] dp = new int[n];
        for (int i = 0; i < n; i++) {
            dp[i] = 1;
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                // dp[j]的表示当前这一行 的dp[j]  dp[j-1]表示上一行的dp[j]
                dp[j] = dp[j] + dp[j - 1];
            }
        }
        return dp[n - 1];
        // 初始化第一列}
    }

2、最小路径和

1)关键解题思路
某网格最小值无非就是公式: // grid[i][j] = min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j] ;
通过累加改变每个网格的值,此时网格的值代表所有路径下最小的和,也就是直接在grid[i][j] 上面直接修改。
2)代码

   public int minPathSum(int[][] grid) {
        // grid[i][j] = min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j] ;
        // 原 grid 矩阵元素中被覆盖为 dp 元素后(都处于当前遍历点的左上方),不会再被使用到
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                if (i == 0 && j == 0) continue;
                else if (i == 0) grid[i][j] += grid[i][j - 1];
                else if ((j == 0)) grid[i][j] += grid[i - 1][j];
                else grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1]);
            }

        }
        return grid[grid.length - 1][grid[0].length - 1];
    }

3、最长回文子串

1)解题关键思路
解题关键方法:中心扩散法

如何理解中心扩散法?
它的基本思想是从每一个可能的“中心”开始,向两边扩展,直到不能再扩展为止。
即为从每一个位置出发,向两边扩散。遇到不是回文时候就直接结束。
例如:“babad”
第二个b位置:2,出发最长回文子串为多少。怎么找?

  1. 先往左寻找与当期位置相同的字符,直到不相等。
  2. 往右寻找与当前位置相同的字符,直到不相等
  3. 最后就进行左右双向扩散,直到左和右不相等。

如图所示
在这里插入图片描述
每个位置向两边扩散都会出现一个窗口大小(len)。如果 len>maxLen(用来表示最长回文串的长度)。则更新 maxLen 的值。
因为我们最后要返回的是具体子串,而不是长度,因此,还需要记录一下 maxLen 时的起始位置(maxStart),即此时还要 maxStart=len。

2)代码

 public String longestPalindrome(String s) {
        int length = s.length();
        if (length == 1) return s;
        // 定义中心扩散中心的左右边界
        int left = 0;
        int right = 0;
        // 当前中心点的回文字串最大长度
        int len = 1;
        // 某结果中心点的最大回文字串的起始索引
        int maxStart = 0;
        // 该结果最大的回文字串长度
        int maxLen = 0;
        for (int i = 0; i < length; i++) {
            // 先进行左右扩散
            left = i-1;
            right = i+1;
            // 左扩散
            while (left >= 0 && s.charAt(left) == s.charAt(i)) {
                left--;
                len++;
            }
            // 右扩散
            while (right < length && s.charAt(right) == s.charAt(i)) {
                right++;
                len++;
            }
            // 左右扩散
            while (right < length && left >= 0 && s.charAt(right) == s.charAt(left)) {
                left--;
                right++;
                len += 2;
            }
            // 最终得到以该i位置为中心的回文字串长度
            if (len > maxLen) {
                maxLen = len;
                // 记录起始位置
                maxStart = left+1;
            }
            // 重置len 为下一个准备
            len = 1;
        }
        return s.substring(maxStart, maxStart + maxLen);
    }

3)分析

  • 最为关键的一点就是 : left = i-1;right = i+1;先提前扩散好,为后续左,右,左右扩散s.charAt(left) == s.charAt(i)等比较做好准备。
  • 因为提前左右扩散,所以最终的left是不符合的,而left+1才是回文子串的起始位置。

4、最长公共子序列

1)解题关键

  • 求公共最长子序列,就是利用dp数组存放之前已经计算过的值依次计算到text1,text2结尾得出结果。
  • 利用二维数组dp[i][j] 表示【0 - t1-1】区间和【0 - t2-1】区间的最长公共子序列,依次存储计算过的值,最终得到结果:dp[t1][t2];

2)代码

 public int longestCommonSubsequence(String text1, String text2) {
        int t1 = text1.length();
        int t2 = text2.length();
        // 代表0 - t1-1和0 - t2-1的最长公共子序列
        int[][] dp = new int[t1 + 1][t2 + 1];
        for (int i = 1; i <= t1; i++) {
            for (int j = 1; j <= t2; j++) {
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
               else{
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[t1][t2];
    }

3)分析

  • 当两个text1.charAt(i - 1) == text2.charAt(j - 1)代表的是最后一个字母相等,此时+1,当不相等时候,等于text1-1和text2或者text1和text2-1比较得到结果。

5、编辑距离

1)解题关键
本题咋一看完全没有思路,其实解决本题的思路主要就是定义二维的动态dp,并理解dp[i][j]:[0,i-1] [0,j-1]区间的最少操作数,思路其实和上一题最长公共子序列长度差不多,无非就是相等时候不用操作,不相等时候进行增删改。
2)代码

  public int minDistance(String word1, String word2) {
        int w1 = word1.length();
        int w2 = word2.length();
        char[] W1 = word1.toCharArray();
        char[] W2 = word2.toCharArray();
        // dp[i][j]:word1[0..i-1]和word2[0..j-1]的最小编辑距离
        int[][] dp = new int[w1 + 1][w2 + 1];
        // 初始化
          // 初始化
        for (int i = 0; i <= w1; i++) {
            dp[i][0] = i;
        }  for (int j = 0; j <= w2; j++) {
            dp[0][j] = j;
        }
        for (int i = 1; i <= w1; i++) {
            for (int j = 1; j <= w2; j++) {
                // 相等 等于之前
                if (W1[i - 1] == W2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    // dp[i - 1][j]:word1删除一个元素,那么就是以下标i - 2为结尾的word1与j-1为结尾的word2的最近编辑距离 再加上一个操作。
                    dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1]) ) + 1;
                }
            }
        }
        return dp[w1][w2];
    }

3)分析
无非就是理解递推公式,相等时候:不需要操作直接得出结果dp[i][j] = dp[i - 1][j - 1];
不相等时候: dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1]) ) + 1;对i索引-1操作或者j-1索引操作,最后替换 i-1,j-1操作,求三个操作最小值。

十七、技巧

十八、排序算法

概述

1、冒泡排序(Bubble Sort)

冒泡排序(Bubble Sort):通过重复遍历要排序的列表,一次比较两个元素,如果他们的顺序错误就把他们交换过来。
1)关键解题思路
2)代码

/* 冒泡排序 */
    void bubbleSort(int[] nums) {
        int length = nums.length;
        if(length<2) return;
        // 只需要遍历length -1遍即可
        for (int i = length -1 ; i >0 ; i--) {
            boolean flag = false;
            for (int j = 0; j < i; j++) {
                if (nums[j] > nums[j + 1]) {
                    int temp = nums[j];
                    nums[j] = nums[j+1];
                    nums[j + 1] = temp;
                    // 每次交换都表示有
                    flag = true;
                }
            }
            // 说明没有进行交换 已经达到有序直接退出
            if (!flag) break;
        }
    }

3)分析
1、时间复杂度
最好情况o(n),最坏情况o(n*n)
2、空间复杂度
o(1)

2、选择排序(Selection Sort)

选择排序(Selection Sort):在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
1)关键解题思路
2)代码

/* 选择排序 */
    void selectionSort(int[] nums) {
        int length = nums.length;
        if(length<2) return;
        // 只需要遍历length -1遍即可
        for (int i = 0; i < length-1; i++) {
            int minIndex = i;
            for (int j = i; j < length; j++) {
                if (nums[minIndex] > nums[j]) {
                    // 如果直接这样是无法记录最小值所在的索引位置的
                    minIndex = j;
                }
            }
            int temp = nums[i];
            nums[i] = nums[minIndex];
            nums[minIndex] = temp;
        }
            // 说明没有进行交换 已经达到有序直接退出
    }

3)分析
1、时间复杂度
o(n*n)
2、空间复杂度
o(1)

3、插入排序

插入排序(Insertion Sort):在已有的有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
1)关键解题思路
2)代码

 /* 插入排序 */
    void insertionSort(int[] nums) {
        int length = nums.length;
        if(length<2) return;
        // 为什么从索引1开始,类似打牌一样,摸到的第一张牌不需要进行排序
        for (int i = 1; i < length; i++) {
            // 待排序元素
            int base = nums[i];
            // 待排序的有序最后的区间
            int j = i - 1;
            // 内循环:将 temp从后向前扫描插入到已排序区间 [0, i-1] 中的正确位置
            while (j >= 0 && base < nums[j]) {
                // 将元素向后面移动
                nums[j + 1] = nums[j];
                j--;
            }
            // 退出循环,此时j是刚好比bse小的 找到插入的索引位置,将其插入
            nums[j + 1] = base;
        }
    }

3)分析
1、时间复杂度

  • 最佳情况为o(n)最坏情况o(n*n)
    2、空间复杂度
    o(1)
    3、理解关键解题思路
    在已排序序列中从后向前扫描,找到相应位置并插入。

4、快速排序(Quick Sort)!!!

快速排序(Quick Sort):也是基于分治法的排序算法,通过一个“基准数”(pivot)将数组分成两部分,左边部分的所有元素都小于基准数,右边部分的所有元素都大于基准数,然后递归地对这两部分进行排序。
1)解题关键思路
选定一个基准数,大于基准数的就排在右边,小于基准数的就排在左边,重复操作
2)代码

 /* 快速排序 */
    void quickSort(int[] nums, int left, int right) {
        if (left >= right) return;
        int baseIndex = partition(nums, left, right);
        quickSort(nums,left,baseIndex-1);
        quickSort(nums,baseIndex+1,right);
    }
    int partition(int[] nums, int left, int right) {
        // 选取第一个元素作为基准
        int base = nums[left];
        while (left < right) {
            while (left < right && nums[right] >= base) {
                right--;
            }
            // 进行左右交换
            // 将较小的元素放到左边
            nums[left] = nums[right];
            while (left < right && nums[left] <= base) {
                left++;
            }
            // 将较大的元素放到右边
            nums[right] = nums[left];
        }
        // 最后将基准元素放到正确的位置
        nums[left] = base;
        return left;
    }

3)分析

  • 选取一个以左边为基准的元素,然后right来移动寻找小于base的数字,然后放到左边 :nums[left] = nums[right];
  • 接下来就是同理可得
    1、时间复杂度为 (nlogn)
    2、空间复杂度为o(n)

5、归并排序(Merge Sort)

归并排序(Merge Sort):采用分治法的一个典型应用,将已经有序子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
1)关键解题思路
在这里插入图片描述

  • 划分阶段:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题。
  • 合并阶段:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束。

2)代码

  /* 归并排序 */
    void mergeSort(int[] nums, int left, int right) {
        // 终止条件
        if (left >= right) return;
        // 递归进行划分阶段
        int mid = (left + right) / 2;
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);
        // 合并左右子数组
        // 左子数组区间为 [left, mid],
        // 右子数组区间为 [mid+1, right]
        merge(nums, left, mid, right);
    }

    private void merge(int[] nums, int left, int mid, int right) {
        int[] temp = new int[right - left + 1];
        // 左数组起始索引
        int i = left;
        // 右数组起始索引
        int j = mid + 1;
        int k = 0;
        // 开始进行左右子数组排序
        while (i <= mid && j <= right) {
            if (nums[i] <= nums[j]) {
                temp[k++] = nums[i];
            }else{
                temp[k++] = nums[j];
            }
        }
        // 最后将剩余的元素复制到临时数组中
        while (i<=mid){
            temp[k++] = nums[i++];
        }while (j<=right){
            temp[k++] = nums[j++];
        }
        // 将临时数组temp元素复制回来到原来区间
        for (int l = 0; k < temp.length; l++) {
            nums[left + l] = temp[l];
        }
    }

3)分析

  • 主要就是对数组进行不断mid分为左右子数组,然后再进行合并
  • 合并阶段主要就是对左右子数组进行合并。
    1、时间复杂度
    o(nlogn)
    2、空间复杂度
    o(n)

6、堆排序(Heap Sort)

堆排序(Heap Sort):利用堆这种数据结构设计的一种排序算法,首先将待排序的数组构建成大顶堆或小顶堆,然后将堆顶元素与末尾元素交换,再重新调整堆,直到排序完成。

1)解题关键
2)代码

/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */
void siftDown(int[] nums, int n, int i) {
    while (true) {
        // 判断节点 i, l, r 中值最大的节点,记为 ma
        int l = 2 * i + 1;
        int r = 2 * i + 2;
        int ma = i;
        if (l < n && nums[l] > nums[ma])
            ma = l;
        if (r < n && nums[r] > nums[ma])
            ma = r;
        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
        if (ma == i)
            break;
        // 交换两节点
        int temp = nums[i];
        nums[i] = nums[ma];
        nums[ma] = temp;
        // 循环向下堆化
        i = ma;
    }
}

/* 堆排序 */
void heapSort(int[] nums) {
    // 建堆操作:堆化除叶节点以外的其他所有节点
    for (int i = nums.length / 2 - 1; i >= 0; i--) {
    // 进行节点调整
        siftDown(nums, nums.length, i);
    }
    // 从堆中提取最大元素,循环 n-1 轮
    for (int i = nums.length - 1; i > 0; i--) {
        // 交换根节点与最右叶节点(交换首元素与尾元素)
        int tmp = nums[0];
        nums[0] = nums[i];
        nums[i] = tmp;
        // 以根节点为起点 调整剩余部分重新形成大顶堆
        // siftDown 操作的目的是调整堆中某个节点,使其成为一个有效的堆 
        // 以为0的值改变了,所以进行0节点调整成堆
        siftDown(nums, i, 0);
    }
}

7、希尔排序(Shell Sort)

希尔排序(Shell Sort):是插入排序的一种更高效的改进版本,也称缩小增量排序,通过将原始列表分割成多个子列表分别进行插入排序来提高效率。
1)关键解题思路
理解分组的含义,以及插入排序的关键解题
2)代码

 public static void shellSort(int[] nums) {
        int length = nums.length;
        if(length<2) return;
        // 进行分组
        for (int gap = length / 2; gap >= 1; gap /= 2) {
            // 对每个分组后的子序列进行插入排序
            for (int i = gap ; i < length; i++) {
                int temp = nums[i];
                int j = i - gap;
                while (j >= 0 && nums[j] > temp) {
                    nums[j + gap] = nums[j];
                    j -= gap;
                }
                nums[j + gap] = temp;
            }
        }
    }

3)分析
1、时间复杂度

  • 最佳情况为:O(n log n) 最坏情况O(n^(3/2))
    2、空间复杂度
    o(1)
    3、理解关键解题思路
    在已排序序列中从后向前扫描,找到相应位置并插入。

8、桶排序(Bucket Sort)

桶排序(Bucket Sort):工作原理是将数组分到有限数量的桶里。每个桶再分别排序(有可能再使用别的排序算法或是递归使用桶排序)。
1)关键解题思路
在这里插入图片描述
将对应的元素放到对应的桶上面去,然后进行在每个桶进行排序。
2)代码

 /* 桶排序 */
    void bucketSort(float[] nums) {
       // 初始化 k = n/2个桶,预期向每个桶分配2个元素
        int k = nums.length / 2;
        List<List<Float>> buckets = new ArrayList<>();
        // 初始化桶
        for (int i = 0; i < k; i++) {
            buckets.add(new ArrayList<>());
        }
        for (float num : nums) {
            // 将元素映射到每一个桶去
            int bucketIndex = (int) (num * k);
            buckets.get(bucketIndex).add(num);
        }
        // 对桶里面的每个元素排序
        for (List<Float> bucket : buckets) {
            Collections.sort(bucket);
        }
        // 合并每个排序好的桶元素
        int index = 0;
        for (List<Float> bucket : buckets) {
            for (Float aFloat : bucket) {
                nums[index++] = aFloat;
            }
        }
    }

9、基数排序(Radix Sort)

基数排序(Radix Sort):根据键值的每位数字来分配桶,从最低有效位开始排序,然后收集;再从次低位开始排序…以此类推,直到最高有效位。

其他相关内容

1. 哈希-矩阵解题心得(一):https://blog.csdn.net/a147775/article/details/143558612

2. 链表-堆解题心得(二):https://blog.csdn.net/a147775/article/details/144218993

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2253192.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

系统--线程互斥

1、相关背景知识 临界资源多线程、多执行流共享的资源,就叫做临界资源临界区每个线程内部,访问临界资源的代码互斥在任何时刻,保证有且只有一个执行流进入临界区,访问临界资源,对临界资源起到保护作用原子性不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么…

Qt桌面应用开发 第十天(综合项目二 翻金币)

目录 1.主场景搭建 1.1重载绘制事件&#xff0c;绘制背景图和标题图片 1.2设置窗口标题&#xff0c;大小&#xff0c;图片 1.3退出按钮对应关闭窗口&#xff0c;连接信号 2.开始按钮创建 2.1封装MyPushButton类 2.2加载按钮上的图片 3.开始按钮跳跃效果 3.1按钮向上跳…

getchar()

getchar():从计算机终端&#xff08;一般是键盘&#xff09;输入一个字符 1、getchar返回的是字符的ASCII码值&#xff08;整数&#xff09;。 2、getchar在读取结束或者失败的时候&#xff0c;会返回EOF 输入密码并确认&#xff1a; scanf读取\n之前的内容即12345678 回车符…

linux 获取公网流量 tcpdump + python + C++

前言 需求为&#xff0c;统计linux上得上下行公网流量&#xff0c;常规得命令如iftop 、sar、ifstat、nload等只能获取流量得大小&#xff0c;不能区分公私网&#xff0c;所以需要通过抓取网络包并排除私网段才能拿到公网流量。下面提供了一些有效得解决思路&#xff0c;提供了…

Node.js:开发和生产之间的区别

Node.js 中的开发和生产没有区别&#xff0c;即&#xff0c;你无需应用任何特定设置即可使 Node.js 在生产配置中工作。但是&#xff0c;npm 注册表中的一些库会识别使用 NODE_ENV 变量并将其默认为 development 设置。始终在设置了 NODE_ENVproduction 的情况下运行 Node.js。…

KAN-Transfomer——基于新型神经网络KAN的时间序列预测

1.数据集介绍 ETT(电变压器温度)&#xff1a;由两个小时级数据集&#xff08;ETTh&#xff09;和两个 15 分钟级数据集&#xff08;ETTm&#xff09;组成。它们中的每一个都包含 2016 年 7 月至 2018 年 7 月的七种石油和电力变压器的负载特征。 traffic(交通) &#xff1a;描…

中安证件OCR识别技术助力鸿蒙生态:智能化证件识别新体验

在数字化和智能化的浪潮中&#xff0c;伴随国产化战略的深入推进&#xff0c;国产操作系统和软件生态的建设逐渐走向成熟。鸿蒙操作系统&#xff08;HarmonyOS Next&#xff09;作为华为推出的重要操作系统&#xff0c;凭借其开放、灵活和高效的特点&#xff0c;正在加速在多个…

Java设计模式之状态模式架构高扩展的订单状态管理

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…

【排序用法】.NET开源 ORM 框架 SqlSugar 系列

&#x1f4a5; .NET开源 ORM 框架 SqlSugar 系列 &#x1f389;&#x1f389;&#x1f389; 【开篇】.NET开源 ORM 框架 SqlSugar 系列【入门必看】.NET开源 ORM 框架 SqlSugar 系列【实体配置】.NET开源 ORM 框架 SqlSugar 系列【Db First】.NET开源 ORM 框架 SqlSugar 系列…

家政小程序开发,打造便捷家政生活小程序

目前&#xff0c;随着社会人就老龄化和生活压力的加重&#xff0c;家政服务市场的需求正在不断上升&#xff0c;家政市场的规模也正在逐渐扩大&#xff0c;发展前景可观。 在市场快速发展的影响下&#xff0c;越来越多的企业开始进入到市场中&#xff0c;同时家政市场布局也发…

自然语言处理:基于BERT预训练模型的中文命名实体识别(使用PyTorch)

命名实体识别&#xff08;NER&#xff09; 命名实体识别&#xff08;Named Entity Recognition, NER&#xff09;是自然语言处理&#xff08;NLP&#xff09;中的一个关键任务&#xff0c;其目标是从文本中识别出具有特定意义的实体&#xff0c;并将其分类到预定义的类别中。这…

掌握 Spring Boot 中的缓存:技术和最佳实践

缓存是一种用于将经常访问的数据临时存储在更快的存储层&#xff08;通常在内存中&#xff09;中的技术&#xff0c;以便可以更快地满足未来对该数据的请求&#xff0c;从而提高应用程序的性能和效率。在 Spring Boot 中&#xff0c;缓存是一种简单而强大的方法&#xff0c;可以…

Unity 模拟百度地图,使用鼠标控制图片在固定区域内放大、缩小、鼠标左键拖拽移动图片

效果展示&#xff1a; 步骤流程&#xff1a; 1.使用的是UGUI&#xff0c;将下面的脚本拖拽到图片上即可。 using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems;public class CheckImage : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragH…

基础入门-Web应用OSS存储负载均衡CDN加速反向代理WAF防护部署影响

知识点&#xff1a; 1、基础入门-Web应用-防护产品-WAF保护 2、基础入门-Web应用-加速服务-CDN节点 3、基础入门-Web应用-文件托管-OSS存储 4、基础入门-Web应用-通讯服务-反向代理 5、基础入门-Web应用-运维安全-负载均衡 一、演示案例-Web-拓展架构-WAF保护-拦截攻击 原理&a…

指针(上)

目录 内存和地址 指针变量和地址 取地址&#xff08;&&#xff09; 解引用&#xff08;*&#xff09; 大小 类型 意义 const修饰 修饰变量 修饰指针 指针运算 指针- 整数 指针-指针 指针的关系运算 野指针 概念 成因 避免 assert断言 指针的使用 strl…

警惕开源信息成为泄密源头

文章目录 前言一、信息公开需谨慎1、警惕采购招标泄密。2、警惕信息公开泄密。3、警惕社交媒体泄密。 二、泄密风险需严防1、健全制度&#xff0c;明确责任。2、加强管控&#xff0c;严格审查。3、提高意识&#xff0c;谨言慎行。 前言 大数据时代&#xff0c;信息在网络空间发…

LearnOpenGL学习(光照 -- 颜色,基础光照,材质,光照贴图)

光照 glm::vec3 lightColor(0.0f, 1.0f, 0.0f); glm::vec3 toyColor(1.0f, 0.5f, 0.31f); glm::vec3 result lightColor * toyColor; // (0.0f, 0.5f, 0.0f); 说明&#xff1a;当我们把光源的颜色与物体的颜色值相乘&#xff0c;所得到的就是这个物体所反射的颜色。 创建…

面向对象(二)——类和对象(上)

1 类的定义 做了关于对象的很多介绍&#xff0c;终于进入代码编写阶段。 本节中重点介绍类和对象的基本定义&#xff0c;属性和方法的基本使用方式。 【示例】类的定义方式 // 每一个源文件必须有且只有一个public class&#xff0c;并且类名和文件名保持一致&#xff01; …

3GPP R18 LTM(L1/L2 Triggered Mobility)是什么鬼?(三) RACH-less LTM cell switch

这篇看下RACH-less LTM cell switch。 相比于RACH-based LTM,RACH-less LTM在进行LTM cell switch之前就要先知道target cell的TA信息,进而才能进行RACH-less过程,这里一般可以通过UE自行测量或者通过RA过程获取,而这里的RA一般是通过PDCCH order过程触发。根据38.300中的描…

实验13 使用预训练resnet18实现CIFAR-10分类

1.数据预处理 首先利用函数transforms.Compose定义了一个预处理函数transform&#xff0c;里面定义了两种操作&#xff0c;一个是将图像转换为Tensor&#xff0c;一个是对图像进行标准化。然后利用函数torchvision.datasets.CIFAR10下载数据集&#xff0c;这个函数有四个常见的…