其他简单算法
与树相关的一些问题。
参考文章:
优先队列(堆)
队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。
这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。
PriorityQueue常用接口
PriorityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,此处主要介绍PriorityQueue。
关于PriorityQueue的使用要注意:
使用时必须导入 PriorityQueue 所在的包,即:
import java.util.PriorityQueue;PriorityQueue 中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException 异常
不能插入 null 对象,否则会抛出 NullPointerException
没有容量限制,可以插入任意多个元素,其内部可以自动扩容。优先级队列的扩容说明:
- 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
 - 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
 - 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容
 
插入和删除元素的时间复杂度为O(logN)
PriorityQueue 底层使用了堆数据结构
PriorityQueue 默认情况下是小堆,即每次获取到的元素都是最小的元素
优先级队列的构造
| 构造器 | 功能介绍+H4:021H4:H4:013 | 
|---|---|
| PriorityQueue() | 创建一个空的优先级队列,默认容量是11 | 
| PriorityOueue(int initialCapacity) | 创建一个初始容量为initialCapacity的优先级队列 注意: initialCapacity不能小于1,否则会抛lllegalArgumentException异常 | 
| PriorityQueue(Collection<? extends E> c) | 用一个集合来创建优先级队列 | 
1  | static void TestPriorityQueue(){  | 
注意:默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户提供比较器
1  | // 用户自己定义的比较器:直接实现Comparator接口,然后重写该接口中的compare方法即可  | 
插入/删除/获取优先级最高的元素
| 函数名 | 功能介绍 | 
|---|---|
| boolean offer(E e) | 插入元素e,插入成功返回true,如果e对象为空,抛出NulIPointerException异常,时间复杂度 注意:空间不够时候会进行扩容  | 
| E peek() | 获取优先级最高的元素,如果优先级队列为空,返回null | 
| E poll()) | 移除优先级最高的元素并返回,如果优先级队列为空,返回null | 
| int size() | 获取有效元素的个数 | 
| void clear() | 清空 | 
| boolean isEmpty() | 检测优先级队列是否为空,空返回true | 
1  | static void TestPriorityQueue2(){  | 
堆的应用
PriorityQueue的实现
用堆作为底层结构封装优先级队列
堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
建堆:升序,建大堆;降序,建小堆
利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
Top-k问题
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都 不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
用数据集合中前K个元素来建堆 前k个最大的元素,则建小堆前k个最小的元素,则建大堆
用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
案例
例题参见 数组中的第K个最大元素
二分查找
二分查找,也称为折半查找(Binary Search),是一种在有序数组中高效查找特定元素的搜索算法,时间复杂度为O(logn)O(logn),通过不断缩小搜索范围实现快速定位。它的基本思想是将目标值与数组中间的元素进行比较,如果目标值小于中间元素,则在数组的左半部分继续查找,否则在右半部分查找,不断缩小搜索范围,直到找到目标值或确定目标值不存在为止。
算法原理
二分查找的核心思想是分治策略,具体步骤如下
- 有序数组前提:仅适用于已排序的数组,利用顺序性快速排除无效区间。
 - 中间元素比较:每次取中间位置元素与目标值比较,若相等则返回位置:
 - 缩小搜索范围
- 若目标值小于中间元素,则在左半区间继续查找;
 - 若目标值大于中间元素,则在右半区间继续查找。
 
 - 终止条件:当搜索区间缩小至空(即左边界超过右边界)时,判定元素不存在。
 
代码实现
1  | int binarySearch(int[] nums, int target) {  | 
代码关键点:
- 循环条件:
left <= right确保单个元素时仍能执行判断。 - 中间值计算:使用
mid = left + (right - left) / 2避免整数溢出风险。 
时间复杂度与空间复杂度
- 时间复杂度:O(logn)O(logn),每次迭代将搜索范围减半。
 - 空间复杂度:迭代实现为O(1)O(1),递归实现因调用栈深度为O(logn)O(logn)。
 
案例
例题参见 寻找两个正序数组的中位数
栈&矩阵&队列
矩阵(Matrix)
概念
矩阵是一个多维数组,其元素以行和列的形式排列。在计算机内存中,矩阵通常被实现为向量中的向量。一个3x3的矩阵有9个位置,每个位置可以通过一对索引(行号和列号)来访问。例如,一个元素 M 可能位于 [0][0] 的位置,而另一个元素 O 可能位于 [2][2] 的位置。矩阵可以是任意维度的,比如四维矩阵、五维矩阵等,但它们通常用于处理复杂的科学计算和图形渲染。
矩阵是数据科学、图像处理、机器学习、计算机图形学等领域最基础、最常用的数据结构之一。在现实世界中,数据往往以二维或多维数组的形式存在。
矩阵转置(Transpose)
矩阵转置(Transpose)是指将矩阵的行与列互换,通常记作 $A^T$。例如,$A$ 为 $m×n$ 矩阵,则 $A^T$ 为 $n×m$ 矩阵,满足 $A^T[i][j] = A[j][i]$。重要性如下:
- 数据变换:在高效存储或算法实现时,经常需要将行主序(row-major)与列主序(column-major)格式互换;
 - 线性代数:在求解矩阵方程、特征值分解等算法中,转置操作会频繁出现;
 - 并行加速:对大规模矩阵做转置时,合理的分块策略和优化手段能大幅提升缓存命中率和并行效率。
 
栈(Stack)
概念
栈是一种特殊的列表,其添加和删除元素的操作仅限于一端,即栈顶。栈的基本操作包括入栈(push)和出栈(pop),遵循后进先出(LIFO)或先进后出(FILO)的原则。栈在程序中用于存储函数调用的上下文、保存局部变量、实现递归函数等。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据在栈顶。
基本操作
| 方法 | 功能 | 
|---|---|
| new Stack() | 创建一个新的空白的栈 | 
| push(a) | 将元素a插入栈中,实现压栈 | 
| pop() | 将栈顶元素进行出栈 | 
| peek() | 获取栈顶元素 | 
| int size() | 获取栈中元素的个数 | 
| boolean isEmpty() | 判断栈中元素是否为空 | 
队列(Queue)
概念
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出,
入队列:进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头
基本操作
在Java中,Queue是个接口,底层是通过链表实现的。
| 方法 | 功能 | 
|---|---|
| boolean offer(E e) | 入队列 | 
| E poll() | 出队列 | 
| peek() | 获取队头元素 | 
| int size() | 获取队列中有效元素个数 | 
| boolean isEmpty() | 检测队列是否为空 | 
注意:Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。
循环队列
解决假溢出的方法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。当队首指针**Q->front = maxsize-1**后,再前进一个位置就自动到0,这可以利用除法取余运算(%)来实现。
- 初始时:Q->front = Q->rear=0
 - 队首指针进1:Q->front = (Q->front + 1) % MAXSIZE
 - 队尾指针进1:Q->rear = (Q->rear + 1) % MAXSIZE
 - 队列长度:(Q->rear - Q->front + MAXSIZE) % MAXSIZE
 
判断队列为空的条件是:Q->front == Q->rear
为了区分队列还是队满的情况,有三种处理方式: (重点是第一种)
- 牺牲一个单元来判断队空还是队满,入队时少用一个单元. 约定以“队头指针在队尾指针的下一位置作为队满的标志”
- 队满条件: (Q->rear + 1)%Maxsize == Q->front
 - 队空条件: Q->front == Q->rear
 - 队列中元素的个数: (Q->rear - Q ->front + Maxsize)% Maxsize
 
 - 使用计数的方法来判断队列是否已满
 - 增设一个boolean flag标记,以区分队满还是队空,flag等于false,若因删除导致 
Q->front == Q->rear,则为队空;flag 等于 true 时,若因插入导致Q ->front == Q->rear,则为队满 
案例
双指针
概念
双指针算法通过维护两个指针(索引或节点引用)在数据结构中协同移动,以优化时间复杂度。其核心在于减少不必要的遍历次数,将原本需要嵌套循环的问题转化为单次遍历,时间复杂度通常从 O(n²) 降低至 O(n) 或 O(n log> n),适用于数组、链表、字符串等线性结构 。
核心特点
双指针协同:两指针根据规则同步或异步移动,覆盖问题解空间。
单调性依赖:多数问题需数据结构具备有序性或其他规律,例如排序后的数组或特定约束的子串。
高效性:通过剪枝或跳过无效区间,避免重复计算。
分类与应用场景
快慢指针
原理:两个指针以不同速度移动,常用于 链表操作 和 循环检测。
应用场景:
- 链表环检测:快指针每次移动两步,慢指针移动一步,若相遇则存在环 。
 - 链表中点查找:快指针到末尾时,慢指针指向中点 。
 - 删除重复元素:慢指针标记唯一元素位置,快指针遍历数组(如有序数组去重)
 
对撞指针(左右指针)
原理:两指针从两端向中间移动,适用于 有序数组查找 或 对称性问题。
应用场景:
- 两数之和:在排序数组中找和为目标值的两个数(指针调整方向基于当前和与目标值的比较) 。
 - 盛水容器:计算最大容积时,移动高度较小的指针以寻求更大可能 。
 - 反转数组/字符串:交换左右指针元素直至相遇
 
滑动窗口
原理:维护动态窗口,通过调整窗口边界寻找最优解,常用于 子串/子数组问题。
应用场景:
- 最小覆盖子串:窗口覆盖目标字符集时收缩左边界以优化长度 。
 - 最长无重复子串:右指针扩展窗口,左指针在重复时跳跃 。
 
案例
例题参见 好子数组的最大分数
枚举
例题参见 超级回文数
模拟
例题参见 螺旋矩阵
子集合
例题参见 求数组的子集合
算法题
数组中的第K个最大元素
题目链接:LeetCode_215. 数组中的第K个最大元素
问题描述:
给定整数数组nums和整数k,请返回数组中第**k**个最大的元素。
请注意,你需要找的是数组排序后的第k个最大的元素,而不是第k个不同的元素。
你必须设计并实现时间复杂度为O(n)的算法解决此问题。示例 1:
 1
2 输入: [3,2,1,5,6,4], k = 2
输出: 5示例 2:
 1
2 输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4提示:
1 <= k <= nums.length <= 10^5-10^4 <= nums[i] <= 10^4
1  | public int findKthLargest(int[] nums, int k) {  | 
寻找两个正序数组的中位数
**问题描述:
**给定两个大小分别为m和n的正序(从小到大)数组nums1和nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为O(log (m+n))。示例 1:
 1
2
3 输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2示例 2:
 1
2
3 输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5提示:
nums1.length == mnums2.length == n0 <= m <= 10000 <= n <= 10001 <= m + n <= 2000-10^6 <= nums1[i], nums2[i] <= 10^6思路:
两个有序数组的长度分别为m和n,由于两个数组长度之和 m+n 的奇偶不确定,因此需要分情况来讨论,对于奇数的情况,直接找到最中间的数即可,偶数的话需要求最中间两个数的平均值。为简化代码,不分情况讨论,使用一个小技巧:分别找第 (m+n+1) / 2 个,和 (m+n+2) / 2 个,然后求其平均值即可,这对奇偶数均适用。假如 m+n 为奇数的话,那么其实 (m+n+1) / 2 和 (m+n+2) / 2 的值相等,相当于两个相同的数字相加再除以2,还是其本身。
定义一个函数用于在两个有序数组中找到第K个元素,下面重点来看如何实现找到第
K个元素。
- 首先,为了避免产生新的数组从而增加时间复杂度,使用两个变量 i 和 j 分别来标记数组 nums1 和 nums2 的起始位置。
 - 然后,处理边界问题,比如当某一个数组的起始位置大于等于其数组长度时,说明其所有数字均已经被淘汰了,相当于一个空数组了,那么实际上就变成了在另一个数组中找数字,直接就可以找出来了。还有如果 K=1 ,那么只要比较 nums1 和 nums2 的起始位置 i 和 j 上的数字就可以了。
 - 难点在于一般的情况怎么处理?因为需要在两个有序数组中找到第K个元素,为了加快搜索的速度,要使用二分法,对 K 二分,意思是要分别在 nums1 和 nums2 中查找第 K/2 个元素,注意这里由于两个数组的长度不定,所以有可能某个数组没有第 K/2 个数字,所以需要先检查数组中到底存不存在第 K/2 个数字,如果存在就取出来,否则就赋值上一个整型最大值。如果某个数组没有第 K/2 个数字,那么淘汰另一个数字的前K/2个数字。有没有可能两个数组都不存在第 K/2 个数字呢,这道题里是不可能的,因为K是给的m+n的中间值,所以必定至少会有一个数组是存在第K/2个数字的。
 - 最后就是二分法的核心,比较这两个数组的第 K/2 小的数字 midVal1 和 midVal2 的大小,如果第一个数组的第 K/2 个数字小的话,那么说明要找的数字肯定不在 nums1 中的前 K/2 个数字,所以可以将其淘汰,将 nums1 的起始位置向后移动 K/2 个,并且此时的K也自减去K/2,调用递归。反之,淘汰nums2中的前K/2个数字,并将 nums2 的起始位置向后移动 K/2 个,并且此时的 K 也自减去K/2,调用递归。
 
1  | class Solution {  | 
最大矩形
题目链接:85. 最大矩形
问题描述:给定一个仅包含
0和1、大小为rows x cols的二维二进制矩阵,找出只包含1的最大矩形,并返回其面积。示例 1:
 1
2
3 输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:6
解释:最大矩形如上图所示。示例 2:
 1
2 输入:matrix = [["0"]]
输出:0示例 3:
 1
2 输入:matrix = [["1"]]
输出:1提示:
rows == matrix.lengthcols == matrix[0].length1 <= row, cols <= 200matrix[i][j]为'0'或'1'
1  | public int maximalRectangle(char[][] matrix) {  | 
柱状图中最大的矩形
题目链接:84. 柱状图中最大的矩形
问题描述:给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例 1:
 1
2 输入:heights = [2,1,5,6,2,3]
输出:10示例 2:
 1
2 输入: heights = [2,4]
输出: 4提示:
1 <= heights.length <=10^50 <= heights[i] <= 10^4思路:使用单调增栈可以 O(1) 的获取柱体 i 左边第一个比它小的柱体,因为对于栈中的柱体来说,栈中的下一个柱体就是其「左边第一个小于自身的柱体」。遍历每个柱体,若当前的柱体 i 高度大于等于栈顶柱体的高度,就直接将当前柱体入栈,否则若当前柱体 i 高度小于栈顶柱体的高度,说明 i 是栈顶柱体的「右边第一个小于栈顶柱体的柱体」,以栈顶柱体为高的矩形的左右宽度边界就确定了,那么就可以将栈顶柱体出栈来计算以其为高的矩形的面积了。
1  | public int largestRectangleArea(int[] heights) {  | 
好子数组的最大分数
问题描述:
给你一个整数数组nums(下标从 0 开始)和一个整数k。一个子数组(i, j)的 分数 定义为min(nums[i], nums[i+1], ..., nums[j]) * (j - i + 1)。一个 好 子数组的两个端点下标需要满足i <= k <= j。请你返回 好 子数组的最大可能 分数 。示例 1:
 1
2
3 输入:nums = [1,4,3,7,4,5], k = 3
输出:15
解释:最优子数组的左右端点下标是 (1, 5) ,分数为 min(4,3,7,4,5) * (5-1+1) = 3 * 5 = 15 。示例 2:
 1
2
3 输入:nums = [5,5,4,5,4,1,1,1], k = 0
输出:20
解释:最优子数组的左右端点下标是 (0, 4) ,分数为 min(5,5,4,5,4) * (4-0+1) = 4 * 5 = 20 。提示:
1 <= nums.length <= 10^51 <= nums[i] <= 2 * 10^40 <= k < nums.length思路:判断左右指针对应的数组值哪个比较大,移动到大的一端即可
1  | public int maximumScore(int[] nums, int k) {  | 
超级回文数
题目链接:LeetCode_906. 超级回文数
问题描述:
如果一个正整数自身是回文数,而且它也是一个回文数的平方,那么我们称这个数为 超级回文数 。现在,给你两个以字符串形式表示的正整数 left 和 right ,统计并返回区间[left, right]中的 超级回文数 的数目。示例 1:
 1
2
3
4 输入:left = "4", right = "1000"
输出:4
解释:4、9、121 和 484 都是超级回文数。
注意 676 不是超级回文数:26 * 26 = 676 ,但是 26 不是回文数。示例 2:
 1
2 输入:left = "1", right = "2"
输出:1提示:
1 <= left.length, right.length <= 18left和right仅由数字(0 - 9)组成。left和right不含前导零。left和right表示的整数在区间[1, 10^18 - 1]内。left小于等于right。
1  | public int superpalindromesInRange(String left, String right) {  | 
螺旋矩阵
题目链接:54. 螺旋矩阵
问题描述:给你一个
m行n列的矩阵matrix,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。示例 1:
 1
2 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]示例 2:
 1
2 输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]提示:
m == matrix.lengthn == matrix[i].length1 <= m, n <= 10-100 <= matrix[i][j] <= 100思路:模拟矩形的顺时针旋转,所以核心依然是依然是坚持循环不变量,按照左闭右开的原则。模拟顺时针画矩阵的过程:
- 填充上行从左到右
 - 填充右列从上到下
 - 填充下行从右到左
 - 填充左列从下到上
 由外向内一圈一圈这么画下去。
设置四个边界,注意状态改变后要增加条件限制,比如后面两个for循环,这个解题是比较好记的,注意每次都是左开右闭就可以了,不要随意更改,其实就是二分查找的区间不变量
1  | public List<Integer> spiralOrder(int[][] matrix) {  | 
求数组的子集合
题目一:给出一个数组 list = [1,2,3,4,5,6],求此数组的所有子集合
输出:[1],[1,2],[1,2,3]…题目二:给出一个数组 list = [1,2,3,4,5,6],求此数组的所有子集合中和为6的子集
输出:[6,[2,4],[1,5],[1,2,3]解题思路:
方式一:递归
方式二:全排列方式
1  | // 递归方式 求数组的子集合,子数组  | 
1  | // 求数组的子集合,子数组 方式二:全排列方式  | 





