DP O(n)遍历 + Binary Search = O(nlogn)

本文介绍了解决LeetCode 300题——最长递增子序列的方法,包括O(n^2)复杂度的动态规划算法实现及更高效的O(nlogn)复杂度算法实现,后者结合了动态规划与二分查找。

  LeetCode 300. Longest Increasing Subsequence

Given an unsorted array of integers, find the length of longest increasing subsequence.

For example,
Given [10, 9, 2, 5, 3, 7, 101, 18],
The longest increasing subsequence is [2, 3, 7, 101], therefore the length is 4. Note that there may be more than one LIS combination, it is only necessary for you to return the length.

Your algorithm should run in O(n2) complexity.

Follow up: Could you improve it to O(n log n) time complexity?

  分析:
  数组乱序,所以一次O(n)遍历所有元素是必需的。
  找出其中最长的递增子序列。所以至少需要两个变量,一个表示递增,一个表示子序列长度。
  假设序列长度为length,现在遍历到第 i 个元素。为了获得递增子序列,可以有几种方法:
  法一:最初想到的是从该第 i 个元素向后遍历,看依次比它大的元素有多少,但是对于比他大的元素,如何选择哪一个元素作为子序列的第几个元素,会产生额外的时间复杂度。再记录子序列长度。这样最后就知道从每一个元素开始向后所得的递增子序列长度。只是复杂度达到O(n^3)。不可取。
  法二:DP。从第 0 个元素遍历到第 i 个元素。另设一个数组dp[length],存储从第 0 到第 i 个元素为止,以第 i 个元素为截止元素的最长递增子序列的长度。对于第 i 个元素,看其前面的元素值是否有比它小的,如果有,并且那个元素值的 (dp[]值 + 1) > dp[i],则更新 dp[i] 值。这样最后单独再遍历一遍 dp[] 取最大值,就知道结果了。
  Java代码如下。

    public int lengthOfLIS(int[] nums) {
        //O(n^2)
        int l = nums.length;
        if(l <= 1)
            return l;
        int[] dp = new int[l];
        Arrays.fill(dp,1);
        for(int i = 0; i<l; i++){
            for(int j = 0; j < i; j++){
                if(nums[i] > nums[j]){
                    if(dp[j] + 1 > dp[i])
                        dp[i] = dp[j] + 1;
                }
            }
        }
        int res = dp[0];
        for(int t : dp)
            res = res > t ? res:t;
        return res;
    }

  法三:DP + Binary Search
  直接引用了他人的做法和思路:
  1, traverse from 0 to len-1, the DP array keep the longest sequence.
  2, if the val is bigger than largest in the dp array, add it to the end;
  3, if it is among the sequence, return the pos that bigger than pres, update the array with this position if val is smaller than dp[pos];
  This is to keep the sequence element with the smallest number.
  
  For example:

10, 9, 2, 5, 3, 7, 101, 18

10 
9
2
2,5
2,3
2,3,7
2,3,7,101
2,3,7,18

  Java代码如下。

public class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        int[] dp = new int[nums.length];//由于可能存在多个最长递增子序列,所以dp可能存储递增最长子序列,也可能不一定。但不影响最长递增子序列的大小返回值
        dp[0] = nums[0];
        int len = 0;
        for (int i = 1; i < nums.length; i++) {
            int pos = binarySearch(dp,len,nums[i]);
            if (nums[i] < dp[pos]) dp[pos] = nums[i];
            if (pos > len) {
                len = pos;
                dp[len] = nums[i];
            }
        }
        return len+1;
    }
    private int binarySearch(int[] dp, int len, int val) {
        int left = 0;
        int right = len;
        while(left+1 < right) {
            int mid = left + (right-left)/2;
            if (dp[mid] == val) {
                return mid;
            } else {
                if (dp[mid] < val) {
                    left = mid;
                } else {
                    right = mid;
                }
            }
        }
        if (dp[right] < val) return len+1;
        else if (dp[left] >= val) return left;
        else return right;
    }
}
Java 常见常用算法详解 Java 作为一门工业级编程语言,其强大的标准库和生态系统内置了大量高效、稳定的算法。同时,理解并能够实现经典算法是程序员的核心能力。本文将从 “直接用” 和 “自己写” 两个维度,系统梳理 Java 开发中的常见算法。 第一部分:开箱即用 — JDK 内置算法 Java 标准库 (java.util 和 java.util.Arrays) 提供了许多现成的算法,它们经过高度优化和严格测试,是日常开发的首选。 1. 排序算法 (Sorting) 核心类: java.util.Collections, java.util.Arrays Collections.sort(List<T> list) 用途: 对 List 集合(如 ArrayList, LinkedList) 进行升序排序。 底层实现: 对于对象集合,它使用一种优化的、稳定的归并排序变体 (TimSort)。稳定性意味着相等元素的相对顺序在排序后保持不变。 时间复杂度: 保证 O(n log n)。 Arrays.sort(int[] a) 用途: 对基本类型数组(如 int[], double[]) 进行排序。 底层实现: 使用双轴快速排序 (Dual-Pivot Quicksort)。该算法是对经典快排的改进,在实践中效率极高。 Arrays.sort(T[] a) 用途: 对对象数组(如 String[], Integer[]) 进行排序。 底层实现: 同样使用 TimSort 算法,保证稳定性和高性能。 示例代码: java import java.util.*; // 1. 对List排序 List<Integer> numbersList = new ArrayList<>(Arrays.asList(23, 5, 42, -1, 99)); Collections.sort(numbersList); System.out.println("Sorted List: " + numbersList); // 输出: Sorted List: [-1, 5, 23, 42, 99] // 2. 对数组排序 int[] numbersArray = {23, 5, 42, -1, 99}; Arrays.sort(numbersArray); System.out.println("Sorted Array: " + Arrays.toString(numbersArray)); // 输出: Sorted Array: [-1, 5, 23, 42, 99] // 3. 自定义排序规则(使用Comparator) List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David"); // 按字符串长度排序 Collections.sort(names, (a, b) -> a.length() - b.length()); // 或使用方法引用:Collections.sort(names, Comparator.comparingInt(String::length)); System.out.println("Sorted by length: " + names); // 输出: Sorted by length: [Bob, Alice, David, Charlie] 2. 搜索算法 (Searching) 核心类: java.util.Collections, java.util.Arrays Collections.binarySearch(List, Key) / Arrays.binarySearch(array, key) 用途: 在已排序的列表或数组中,使用二分查找算法快速定位元素。 重要前提: 集合或数组必须是有序的(通常是升序),否则结果不可预测。 返回值: 如果找到,返回元素的索引;如果未找到,返回一个负值,表示应插入的位置 (-(insertion point) - 1)。 时间复杂度: O(log n)。 示例代码: java List<Integer> sortedList = Arrays.asList(10, 20, 30, 40, 50); int index1 = Collections.binarySearch(sortedList, 30); System.out.println("Index of 30: " + index1); // 输出: 2 (找到了) int index2 = Collections.binarySearch(sortedList, 25); System.out.println("Index of 25: " + index2); // 输出: -3 (未找到。插入点应为 2, 所以返回 -2-1 = -3) int[] sortedArray = {10, 20, 30, 40, 50}; int index3 = Arrays.binarySearch(sortedArray, 40); System.out.println("Index of 40 in array: " + index3); // 输出: 3 3. 洗牌、填充与工具算法 核心类: java.util.Collections Collections.shuffle(List) 用途: 随机打乱列表中元素的顺序(洗牌)。 底层实现: 使用 Fisher-Yates shuffle 算法的高效变体,能产生均匀的随机排列。 Collections.reverse(List): 反转列表。 Collections.fill(List, obj): 用指定对象填充列表的所有元素。 Collections.copy(destList, srcList): 复制列表。 Collections.max(Collection) / Collections.min(Collection): 根据自然顺序查找最大/最小元素。 Collections.frequency(Collection, Object): 计算某元素出现的频率。 示例代码: java List<Integer> cards = new ArrayList<>(); for (int i = 1; i <= 10; i++) { cards.add(i); } System.out.println("Original deck: " + cards); Collections.shuffle(cards); System.out.println("Shuffled deck: " + cards); // 其他工具方法 Collections.reverse(cards); System.out.println("Reversed deck: " + cards); int max = Collections.max(cards); int frequencyOfFive = Collections.frequency(cards, 5); System.out.println("Max card: " + max + ", Frequency of 5: " + frequencyOfFive); 第二部分:核心基础 — 需要掌握的经典算法 虽然 JDK 提供了强大的工具,但许多算法思想需要开发者自己实现来解决特定问题。 1. 排序与搜索基础 理解这些基础算法的实现有助于深入理解算法思想。 冒泡排序 (Bubble Sort) 思想: 重复遍历列表,比较相邻元素,如果顺序错误就交换它们。 复杂度: O()。(仅用于教学,实际开发切勿使用!) java public static void bubbleSort(int[] arr) { int n = arr.length; for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j + 1]) { // 交换 arr[j] 和 arr[j+1] int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } 线性搜索 (Linear Search) 思想: 从头到尾遍历每个元素,直到找到目标。 复杂度: O(n)。适用于小规模或未排序的数据。 java public static int linearSearch(int[] arr, int target) { for (int i = 0; i < arr.length; i++) { if (arr[i] == target) { return i; // 找到,返回索引 } } return -1; // 未找到 } 2. 递归与分治 (Recursion & Divide and Conquer) 许多高效算法基于此思想。 经典案例:斐波那契数列 (Fibonacci Sequence) 问题: F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2) (n>=2)。 java // 简单递归(效率极低,存在大量重复计算) public static int fibonacciRecursive(int n) { if (n <= 1) return n; return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2); } // 使用动态规划(迭代+记忆化,高效) public static int fibonacciDP(int n) { if (n <= 1) return n; int[] dp = new int[n + 1]; dp[0] = 0; dp[1] = 1; for (int i = 2; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; } 3. 图算法 (Graph Algorithms) Java 标准库没有图结构,需要自行建模(使用邻接表或邻接矩阵)并实现算法。 图的表示: java // 使用邻接表(最常用) // 1. 使用 Map 和 List Map<Integer, List<Integer>> graph = new HashMap<>(); // 2. 或创建一个 Node 类 class GraphNode { int val; List<GraphNode> neighbors; GraphNode(int x) { val = x; neighbors = new ArrayList<>(); } } // 使用二维数组(邻接矩阵)表示带权图 int[][] graphMatrix; 广度优先搜索 (BFS) - 寻找最短路径(无权图) 思想: 层层扩散,使用队列辅助。 java public int bfsShortestPath(Map<Integer, List<Integer>> graph, int start, int end) { Queue<Integer> queue = new LinkedList<>(); Set<Integer> visited = new HashSet<>(); Map<Integer, Integer> distance = new HashMap<>(); // 记录到起点的距离 queue.offer(start); visited.add(start); distance.put(start, 0); while (!queue.isEmpty()) { int currentNode = queue.poll(); if (currentNode == end) { return distance.get(currentNode); } for (int neighbor : graph.getOrDefault(currentNode, new ArrayList<>())) { if (!visited.contains(neighbor)) { visited.add(neighbor); queue.offer(neighbor); distance.put(neighbor, distance.get(currentNode) + 1); } } } return -1; // 未找到路径 } 4. 动态规划 (Dynamic Programming) 通过存储子问题的解来避免重复计算,从而高效解决复杂问题。 经典案例:爬楼梯问题 问题: 每次可以爬 1 或 2 个台阶,爬到 n 阶有多少种不同方法? 状态转移方程: dp[i] = dp[i-1] + dp[i-2] java public int climbStairs(int n) { if (n <= 2) return n; int[] dp = new int[n + 1]; dp[1] = 1; dp[2] = 2; for (int i = 3; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; } // 可以进一步优化空间复杂度到 O(1),只保留前两个状态 总结与实践建议 场景 推荐做法 对集合/数组排序 永远优先使用 Collections.sort() 或 Arrays.sort()。 在有序数据中查找 使用 binarySearch()。 需要随机顺序 使用 Collections.shuffle()。 解决特定领域问题 (如最短路径、背包问题) 1. 首先寻找优秀的第三方库 (如 JGraphT for 图算法)。 2. 其次再考虑自己实现经典算法。 面试与学习 必须掌握如何从零实现各类经典算法 (快排、归并、BFS/DFS、DP)。 性能优化 理解算法复杂度 (Big O),这是选择合适算法和数据结构的根本依据。 核心思想: 不要重复造轮子。 在日常业务开发中,最大限度地利用 JDK 和成熟第三方库提供的稳定高效的算法实现。你的精力应该集中在正确地建模业务问题和选择最合适的工具(算法/数据结构) 上,而不是重新实现一个可能更差的排序算法。然而,深入理解这些轮子是如何造出来的,是你在遇到复杂问题、需要进行底层优化或通过技术面试时的必备能力。生成思维导图
最新发布
09-16
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值