581. 最短无序连续子数组

本文介绍了一种算法思路,通过单调栈来寻找整数数组中最短的连续子数组,使其升序排序后能使整个数组有序。通过左右两次遍历并维护栈,找到起始和结束下标,计算出最短子数组长度。

题目

给你一个整数数组 nums ,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。
请你找出符合题意的 最短 子数组,并输出它的长度。
在这里插入图片描述

思路

如果数组nums 存在无序子数组,则在从左到右遍历数组的过程中会遇到当前元素小于已经遍历过的元素的情况,在从右到左遍历数组的过程中会遇到当前元素大于已经遍历过的元素的情况,即遍历到的元素的大小不符合单调性。由于和单调性有关,因此可以使用单调栈得到最短无序子数组。

单调栈存储数组 nums 的下标。在从左到右遍历数组的过程中,从栈底到栈顶的下标对应的元素单调递增(非严格);在从右到左遍历数组的过程中,从栈底到栈顶的下标对应的元素单调递减(非严格)。
使用start 和 end 分别表示最短无序子数组的开始下标和结束下标,初始时 start 为数组的长度,end 为−1,即初始值都在数组的下标范围以外。
从左到右遍历数组 nums,当遍历到下标 i 时,进行如下操作:
如果栈不为空且栈顶下标对应的元素大于nums[i],则栈顶下标位于无序子数组中,将栈顶下标出栈,如果出栈下标小于 start 则用出栈下标更新start 的值,重复该操作直到栈为空或者栈顶下标对应的元素小于等于nums[i];将 i入栈。遍历结束之后即可得到最短无序子数组的开始下标 start。
从右到左遍历数组 nums,当遍历到下标 i 时,进行如下操作:
如果栈不为空且栈顶下标对应的元素小于nums[i],则栈顶下标位于无序子数组中,将栈顶下标出栈,如果出栈下标大于end 则用出栈下标更新 end 的值,重复该操作直到栈为空或者栈顶下标对应的元素大于等于nums[i];将 i入栈。遍历结束之后即可得到最短无序子数组的结束下标 end。
得到最短无序子数组的开始下标start 和结束下标 end 之后,即可计算最短无序子数组的长度:
如果start>end,则在遍历过程中没有更新过 start 和 end,最短无序子数组的长度是 0;
如果 start≤end,则最短无序子数组的下标范围是 [start,end],长度是 end−start+1。

代码
class Solution {
    public int findUnsortedSubarray(int[] nums) {
        int len = nums.length;
        //从左到右遍历(栈顶到栈底:从大到小),找出小于前面元素的坐标start
        //从右向左遍历(栈顶到栈底:从小到大),找出大于后面元素的坐标end
        int start = len,end = -1;
        Deque<Integer> leftStack = new LinkedList<>();
        Deque<Integer> rightStack = new LinkedList<>();
        leftStack.push(0);
        for(int i = 1; i < len; i++){
            while(!leftStack.isEmpty() && nums[i] < nums[leftStack.peek()]){
                start = Math.min(start,leftStack.peek());
                leftStack.pop();
            }
            leftStack.push(i);
        }
        rightStack.push(len-1);
        for(int i = len-2; i >= 0; i--){
            while(!rightStack.isEmpty() && nums[i] > nums[rightStack.peek()]){
                end = Math.max(end,rightStack.peek());
                rightStack.pop();
            }
            rightStack.push(i);
        }
        return start > end ? 0 : end - start + 1;
         
    }
}
以下是针对 **数据结构算法 20 道高频面试题** 的标准面试答案详解,符合中高级 Java 开发工程师在真实技术面试中的表达规范:逻辑清晰、原理深入、代码准确、语言专业。每道题均包含 **核心概念 + 实现机制 + 时间/空间复杂度 + 应用场景 + 注意事项**,便于精准作答。 --- ### 1. 数组与链表的区别? | 特性 | **数组(Array)** | **链表(Linked List)** | |------|------------------|------------------------| | 存储方式 | 连续内存空间 | 节点通过指针链接,非连续 | | 访问性能 | 支持随机访问(O(1)) | 只能顺序访问(O(n)) | | 插入删除 | 中间操作需移动元素(O(n)) | 修改指针即可(O(1),前提是定位到节点) | | 扩容机制 | 固定大小或动态扩容(如 ArrayList 扩容为 1.5 倍) | 动态分配节点,无需预分配 | | 内存开销 | 少(仅存储数据) | 多(每个节点额外存储指针) | > ✅ 选择建议: - 查找多 → 用数组 - 插入删除频繁 → 用链表 --- ### 2. 栈和队列的实现? #### 栈(Stack)——后进先出(LIFO) ```java class MyStack<T> { private List<T> data = new ArrayList<>(); public void push(T item) { data.add(item); } public T pop() { if (isEmpty()) throw new RuntimeException("Stack is empty"); return data.remove(data.size() - 1); } public T peek() { if (isEmpty()) throw new RuntimeException("Stack is empty"); return data.get(data.size() - 1); } public boolean isEmpty() { return data.isEmpty(); } } ``` #### 队列(Queue)——先进先出(FIFO) ```java class MyQueue<T> { private LinkedList<T> data = new LinkedList<>(); public void offer(T item) { data.addLast(item); } public T poll() { if (isEmpty()) throw new RuntimeException("Queue is empty"); return data.removeFirst(); } public T peek() { if (isEmpty()) throw new RuntimeException("Queue is empty"); return data.getFirst(); } public boolean isEmpty() { return data.isEmpty(); } } ``` > ✅ 实际开发推荐使用 `Deque` 实现双端队列功能 --- ### 3. 二叉树的遍历方式? 二叉树有四种主要遍历方式: | 遍历类型 | 顺序 | 说明 | |---------|------|------| | **前序(Pre-order)** | 根 → 左 → 右 | 用于复制树、序列化 | | **中序(In-order)** | 左 → 根 → 右 | BST 中序输出有序序列 | | **后序(Post-order)** | 左 → 右 → 根 | 适用于释放内存、求表达式值 | | **层序(Level-order)** | 按层从左到右 | 使用队列实现广度优先搜索 | ```java // 递归前序遍历 public void preorder(TreeNode root, List<Integer> res) { if (root == null) return; res.add(root.val); preorder(root.left, res); preorder(root.right, res); } // 层序遍历(BFS) public List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> result = new ArrayList<>(); if (root == null) return result; Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { int size = queue.size(); List<Integer> level = new ArrayList<>(); for (int i = 0; i < size; i++) { TreeNode node = queue.poll(); level.add(node.val); if (node.left != null) queue.offer(node.left); if (node.right != null) queue.offer(node.right); } result.add(level); } return result; } ``` > ✅ 所有递归遍历均可改为栈模拟的迭代写法 --- ### 4. 图的遍历算法? 图的两种基本遍历算法: #### 深度优先搜索(DFS) ```java public void dfs(int v, boolean[] visited, List<List<Integer>> adj) { visited[v] = true; System.out.print(v + " "); for (int neighbor : adj.get(v)) { if (!visited[neighbor]) { dfs(neighbor, visited, adj); } } } ``` #### 广度优先搜索(BFS) ```java public void bfs(int start, List<List<Integer>> adj, int n) { boolean[] visited = new boolean[n]; Queue<Integer> queue = new LinkedList<>(); visited[start] = true; queue.offer(start); while (!queue.isEmpty()) { int v = queue.poll(); System.out.print(v + " "); for (int neighbor : adj.get(v)) { if (!visited[neighbor]) { visited[neighbor] = true; queue.offer(neighbor); } } } } ``` > ✅ DFS 适合路径探索,BFS 适合路径(无权图) --- ### 5. 排序算法的实现与复杂度? | 算法 | 好 | 平均 | 坏 | 空间 | 是否稳定 | 说明 | |------|------|------|------|------|----------|------| | **冒泡排序** | O(n) | O(n²) | O(n²) | O(1) | 是 | 相邻比较,可优化提前退出 | | **选择排序** | O(n²) | O(n²) | O(n²) | O(1) | 否 | 每次选小放前面 | | **插入排序** | O(n) | O(n²) | O(n²) | O(1) | 是 | 适合小规模或近有序数据 | | **快速排序** | O(n log n) | O(n log n) | O(n²) | O(log n) | 否 | 分治+分区,平均快 | | **归并排序** | O(n log n) | O(n log n) | O(n log n) | O(n) | 是 | 稳定,适合链表 | | **堆排序** | O(n log n) | O(n log n) | O(n log n) | O(1) | 否 | 利用大堆性质 | | **计数排序** | O(n+k) | O(n+k) | O(n+k) | O(k) | 是 | 适用于整数且范围小 | | **桶排序** | O(n) | O(n) | O(n²) | O(n) | 是 | 数据均匀分布时高效 | | **基数排序** | O(d*(n+k)) | 同上 | 同上 | O(k) | 是 | 按位排序,d 为位数 | > ✅ Java 中 `Arrays.sort()` 对基本类型用双轴快排,对象用 TimSort(归并优化) --- ### 6. 查找算法的实现? #### 顺序查找(线性查找) ```java public int linearSearch(int[] arr, int target) { for (int i = 0; i < arr.length; i++) { if (arr[i] == target) return i; } return -1; } ``` > O(n),无序数组可用 #### 二分查找(Binary Search) ```java public int binarySearch(int[] arr, int target) { int left = 0, right = arr.length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (arr[mid] == target) return mid; else if (arr[mid] < target) left = mid + 1; else right = mid - 1; } return -1; } ``` > O(log n),要求有序数组 --- ### 7. 哈希表的实现? 哈希表基于“键→索引”映射实现 O(1) 查找。 #### 核心组件: - **哈希函数**:将 key 映射为数组下标(如 `hashCode() % tableSize`) - **冲突解决**: - **链地址法**(Java HashMap 使用):桶内用链表或红黑树存储多个 entry - **开放寻址法**:线性探测、二次探测等 ```java class SimpleHashMap<K, V> { private static class Entry<K, V> { K key; V value; Entry<K, V> next; Entry(K key, V value) { this.key = key; this.value = value; } } private List<Entry<K, V>>[] table; private int size = 16; @SuppressWarnings("unchecked") public SimpleHashMap() { table = new LinkedList[size]; for (int i = 0; i < size; i++) { table[i] = new LinkedList<>(); } } private int hash(K key) { return Math.abs(key.hashCode()) % size; } public void put(K key, V value) { int index = hash(key); for (Entry<K, V> entry : table[index]) { if (entry.key.equals(key)) { entry.value = value; return; } } table[index].add(new Entry<>(key, value)); } public V get(K key) { int index = hash(key); for (Entry<K, V> entry : table[index]) { if (entry.key.equals(key)) { return entry.value; } } return null; } } ``` > ✅ Java 8 中 HashMap 当链表长度 > 8 且总容量 ≥ 64 时转为红黑树 --- ### 8. 动态规划的解题思路? 动态规划(DP)用于解决具有重叠子问题和优子结构的问题。 #### 解题四步法: 1. **定义状态**:明确 `dp[i]` 或 `dp[i][j]` 表示什么 2. **状态转移方程**:找出当前状态如何由之前状态推导 3. **初始化边界条件** 4. **确定遍历顺序** #### 示例:斐波那契数列 ```java public int fib(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]; } ``` > 可优化为滚动变量 → 空间 O(1) --- ### 9. 贪心算法的应用? 贪心算法在每一步选择当前优解,期望终全局优。 #### 经典应用: - **活动选择问题**:按结束时间排序,选不冲突的大集合 - **霍夫曼编码**:构建优前缀码 - **小生成树(Prim/Kruskal)**:局部小边构造 MST - **找零钱问题**(特定币种下成立) ```java // 活动选择(贪心策略:早结束优先) public int maxActivities(int[] start, int[] end) { int n = start.length; List<Activity> activities = new ArrayList<>(); for (int i = 0; i < n; i++) { activities.add(new Activity(start[i], end[i])); } activities.sort((a, b) -> a.end - b.end); int count = 1, lastEnd = activities.get(0).end; for (int i = 1; i < n; i++) { if (activities.get(i).start >= lastEnd) { count++; lastEnd = activities.get(i).end; } } return count; } ``` > ⚠️ 贪心不一定能得到优解,需数学证明 --- ### 10. 分治算法的典型应用? 分治法将问题分解为独立子问题,分别求解后再合并。 #### 典型应用: - **归并排序**:拆分 → 排序 → 合并 - **快速排序**:选 pivot → 分区 → 递归 - **大整数乘法(Karatsuba)** - **近点对问题** ```java // 归并排序核心 merge private void merge(int[] arr, int l, int m, int r) { int[] temp = new int[r - l + 1]; int i = l, j = m + 1, k = 0; while (i <= m && j <= r) { temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++]; } while (i <= m) temp[k++] = arr[i++]; while (j <= r) temp[k++] = arr[j++]; System.arraycopy(temp, 0, arr, l, temp.length); } ``` > ✅ 分治三步:分解 → 解决 → 合并 --- ### 11. 回溯算法的实现? 回溯是暴力搜索的一种,通过试错寻找所有可行解。 #### 典型问题: - N 皇后 - 八数码 - 组合、排列、子集生成 ```java // 子集生成(回溯模板) public List<List<Integer>> subsets(int[] nums) { List<List<Integer>> result = new ArrayList<>(); backtrack(result, new ArrayList<>(), nums, 0); return result; } private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, int start) { result.add(new ArrayList<>(path)); // 每个节点都是解 for (int i = start; i < nums.length; i++) { path.add(nums[i]); backtrack(result, path, nums, i + 1); path.remove(path.size() - 1); // 撤销选择 } } ``` > ✅ 回溯 = DFS + 状态重置 --- ### 12. 字符串匹配算法? #### 暴力匹配(Brute Force) ```java public int strStr(String text, String pattern) { int n = text.length(), m = pattern.length(); for (int i = 0; i <= n - m; i++) { int j; for (j = 0; j < m; j++) { if (text.charAt(i + j) != pattern.charAt(j)) break; } if (j == m) return i; } return -1; } ``` > O(nm) #### KMP 算法(Knuth-Morris-Pratt) 利用部分匹配表(next 数组)避免重复比较,O(n + m) ```java public int kmpSearch(String text, String pattern) { if (pattern.isEmpty()) return 0; int[] next = buildNext(pattern); int j = 0; for (int i = 0; i < text.length(); i++) { while (j > 0 && text.charAt(i) != pattern.charAt(j)) { j = next[j - 1]; } if (text.charAt(i) == pattern.charAt(j)) { j++; } if (j == pattern.length()) { return i - j + 1; } } return -1; } private int[] buildNext(String pattern) { int[] next = new int[pattern.length()]; int j = 0; for (int i = 1; i < pattern.length(); i++) { while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) { j = next[j - 1]; } if (pattern.charAt(i) == pattern.charAt(j)) { j++; } next[i] = j; } return next; } ``` > ✅ KMP 核心思想:利用已匹配部分的长前后缀进行跳转 --- ### 13. 路径算法? #### Dijkstra(单源路径,非负权) ```java public int[] dijkstra(int[][] graph, int start) { int n = graph.length; int[] dist = new int[n]; boolean[] visited = new boolean[n]; Arrays.fill(dist, Integer.MAX_VALUE); dist[start] = 0; for (int i = 0; i < n; i++) { int u = -1; for (int j = 0; j < n; j++) { if (!visited[j] && (u == -1 || dist[j] < dist[u])) { u = j; } } visited[u] = true; for (int v = 0; v < n; v++) { if (graph[u][v] != 0 && !visited[v]) { int alt = dist[u] + graph[u][v]; if (alt < dist[v]) { dist[v] = alt; } } } } return dist; } ``` > O(V²),可用优先队列优化至 O((V+E)logV) #### Floyd-Warshall(多源路径) ```java public int[][] floydWarshall(int[][] graph) { int n = graph.length; int[][] dist = new int[n][n]; for (int i = 0; i < n; i++) System.arraycopy(graph[i], 0, dist[i], 0, n); for (int k = 0; k < n; k++) for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) if (dist[i][k] != Integer.MAX_VALUE && dist[k][j] != Integer.MAX_VALUE) dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]); return dist; } ``` > O(V³) --- ### 14. 小生成树算法? #### Kruskal 算法(边贪心 + 并查集) ```java class Edge implements Comparable<Edge> { int src, dest, weight; public int compareTo(Edge other) { return this.weight - other.weight; } } public List<Edge> kruskalMST(List<Edge> edges, int V) { Collections.sort(edges); UnionFind uf = new UnionFind(V); List<Edge> result = new ArrayList<>(); for (Edge e : edges) { if (uf.find(e.src) != uf.find(e.dest)) { result.add(e); uf.union(e.src, e.dest); } } return result; } ``` #### Prim 算法(点贪心 + 优先队列) ```java public int primMST(int[][] graph, int start) { int n = graph.length; PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]); // [vertex, weight] boolean[] inMST = new boolean[n]; pq.offer(new int[]{start, 0}); int totalWeight = 0; while (!pq.isEmpty()) { int[] curr = pq.poll(); int u = curr[0], w = curr[1]; if (inMST[u]) continue; inMST[u] = true; totalWeight += w; for (int v = 0; v < n; v++) { if (graph[u][v] > 0 && !inMST[v]) { pq.offer(new int[]{v, graph[u][v]}); } } } return totalWeight; } ``` > ✅ Kruskal 适合稀疏图,Prim 适合稠密图 --- ### 15. 红黑树的性质? 红黑树是一种自平衡二叉查找树,保证坏情况 O(log n) 操作。 #### 五大性质: 1. 每个节点是红色或黑色 2. 根节点是黑色 3. 所有叶子(NIL)是黑色 4. 红色节点的子节点必须是黑色(不能有两个连续红节点) 5. 从任一节点到其每个叶子的所有路径包含相同数目的黑节点(黑高度一致) > ✅ Java 中 `TreeMap` 和 `TreeSet` 基于红黑树实现 --- ### 16. B树和B+树的区别? | 特性 | **B树** | **B+树** | |------|--------|---------| | 数据存储 | 所有节点都存数据 | 只有叶子节点存数据,内部节点只存索引 | | 叶子节点 | 不相连 | 通过链表相连,支持范围查询 | | 查询效率 | 定位快,但范围查询慢 | 范围查询高效 | | 高度 | 相对较高 | 更矮更宽,I/O 更少 | | 应用场景 | 文件系统 | 数据库索引(MySQL InnoDB) | > ✅ B+树更适合磁盘存储,减少 I/O 次数 --- ### 17. 堆排序的实现? 堆排序基于大堆(或小堆)实现原地排序。 ```java public void heapSort(int[] arr) { int n = arr.length; // 构建大堆(从后一个非叶子节点开始) for (int i = n / 2 - 1; i >= 0; i--) { heapify(arr, n, i); } // 逐个提取大元素放到末尾 for (int i = n - 1; i > 0; i--) { swap(arr, 0, i); heapify(arr, i, 0); } } private void heapify(int[] arr, int n, int i) { int largest = i; int left = 2 * i + 1; int right = 2 * i + 2; if (left < n && arr[left] > arr[largest]) largest = left; if (right < n && arr[right] > arr[largest]) largest = right; if (largest != i) { swap(arr, i, largest); heapify(arr, n, largest); } } private void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } ``` > 时间 O(n log n),空间 O(1),不稳定 --- ### 18. 快速排序的优化? #### 原始快排: ```java public void quickSort(int[] arr, int low, int high) { if (low < high) { int pi = partition(arr, low, high); quickSort(arr, low, pi - 1); quickSort(arr, pi + 1, high); } } ``` #### 优化策略: 1. **三数取中法选 pivot**:避免极端情况 2. **小数组改用插入排序** 3. **双轴快排(Dual-Pivot)**:JDK7 Arrays.sort() 使用 4. **尾递归优化**:减少栈深度 ```java private int partition(int[] arr, int low, int high) { int mid = (low + high) / 2; if (arr[mid] < arr[low]) swap(arr, low, mid); if (arr[high] < arr[low]) swap(arr, low, high); if (arr[high] < arr[mid]) swap(arr, mid, high); swap(arr, mid, high); // 将中位数移到末尾作为 pivot int pivot = arr[high]; int i = low - 1; for (int j = low; j < high; j++) { if (arr[j] <= pivot) { i++; swap(arr, i, j); } } swap(arr, i + 1, high); return i + 1; } ``` > ✅ 快排平均快,但坏 O(n²) --- ### 19. 二分查找的变种? #### 查找第一个等于 target 的位置 ```java public int lowerBound(int[] arr, int target) { int left = 0, right = arr.length; while (left < right) { int mid = left + (right - left) / 2; if (arr[mid] < target) { left = mid + 1; } else { right = mid; } } return left; } ``` #### 查找后一个等于 target 的位置 ```java public int upperBound(int[] arr, int target) { int left = 0, right = arr.length; while (left < right) { int mid = left + (right - left) / 2; if (arr[mid] <= target) { left = mid + 1; } else { right = mid; } } return left - 1; } ``` > ✅ 可用于统计某个值的出现次数:`upper - lower + 1` --- ### 20. 大数相加的实现? 模拟竖式加法,处理进位。 ```java public String addStrings(String num1, String num2) { StringBuilder res = new StringBuilder(); int i = num1.length() - 1, j = num2.length() - 1; int carry = 0; while (i >= 0 || j >= 0 || carry > 0) { int x = i >= 0 ? num1.charAt(i--) - '0' : 0; int y = j >= 0 ? num2.charAt(j--) - '0' : 0; int sum = x + y + carry; res.append(sum % 10); carry = sum / 10; } return res.reverse().toString(); } ``` > ✅ 可扩展至大数乘法、减法、除法 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值