基础算法1链接
目录
最长公共前缀
两数之和
删除字符串中所有相邻重复项
n叉树的层序遍历
最后一块石头的重量
第N个泰波那契数
图像渲染
迷宫中离入口最近的出口
矩阵
课程表
14. 最长公共前缀 - 力扣(LeetCode)
在解决这道题时,巧妙运用String
类的两个方法,能让解题过程变得十分轻松。
首先,我们需要确定一个查找公共前缀的标准。这里,我们选择数组中的第一个字符串作为标准。不过,在此之前,必须对边界情况进行预处理。若传入的数组为空或者为null
,应当直接返回空字符串""
。这一步至关重要,能有效避免后续代码在尝试访问strs[0]
时抛出异常。
接着,我们开始遍历数组中的每一个元素。对于数组中的每个字符串,都要进行严格的校验。具体做法是使用indexOf
方法来判断当前的公共前缀prefix
在该字符串中首次出现的下标位置。若这个下标不为 0,那就表明prefix
并非该字符串的前缀。此时,我们需要借助substring
方法来缩短prefix
,继续尝试寻找合适的公共前缀。若str[i].indexOf(prefix) == 0
,这意味着prefix
是该字符串的前缀,我们可以继续对下一个数组元素进行校验。
在使用substring
方法时,若其起始位置和结束位置相等,那就说明数组中存在某两个字符串没有公共前缀,此时应直接返回空字符串""
。
class Solution {
public String longestCommonPrefix(String[] strs) {
//要进行预处理,如果不进行处理,strs为空 strs[0]会抛异常
if(strs == null || strs.length == 0) return "";
String prefix = strs[0];
for(int i = 0; i < strs.length; i++){
//查找字符第一次的位置 如果不等于0则缩小范围
while(strs[i].indexOf(prefix) != 0){
//如果起始位置和结束位置为空 则返回""
prefix = prefix.substring(0,prefix.length() -1);
if(prefix.isEmpty()){
return "";
}
}
}
return prefix;
}
}
解法二:统一比较的策略来寻找公共前缀。从 prefix
的起始位置 0 开始,将该位置的字符依次与数组中每个元素的 0 下标字符进行比较。具体而言,若比较的字符相等,就将该字符 ch
拼接到字符串 s
中。这一过程持续进行,不断检查每个数组元素对应位置的字符。
然而,一旦出现以下两种情况,就需要停止比较并返回结果。其一,当在比较过程中发现有某个数组元素对应位置的字符与 prefix
当前位置的字符不相等时,意味着该字符并非所有字符串的公共前缀;其二,若在比较过程中,某个字符串已经越界,也就是该字符串的长度不足以提供当前比较位置的字符,同样说明该字符不是所有字符串的公共前缀。在这两种情况下,我们只需返回在该字符之前拼接好的字符串即可
class Solution {
public String longestCommonPrefix(String[] strs) {
String s = "";
String prefix = strs[0];
for(int i = 0; i < prefix.length();i++){
char ch = prefix.charAt(i);
for(int j = 0; j < strs.length; j++){
//i >= strs[j].length()必须加上,否则字符串可能会越界
if(i >= strs[j].length() || strs[j].charAt(i) != ch) return s;
}
//没有返回则说明ch是每一个元素的前缀
s += ch;
}
return s;
}
}
解法三:采用两两比较的策略来解决问题。首先,我们需要明确一个标准作为比较的起始点。之后,按照两两一组的方式,在数组元素间寻找公共前缀。
具体操作通过 findCommon
方法实现。在这个方法里,我们知道指针 i
的移动范围是有限的,它不可能超过参与比较的两个字符串中较短字符串的长度。在比较过程中,若两个字符串对应位置的字符相等,就继续向后移动指针 i
,持续进行比较。当出现对应字符不相等的情况,或者指针 i
超出了较短字符串的界限时,比较停止。此时,我们可以从 s1
和 s2
中截取从 0 到 i
下标的子串作为这两个字符串的公共前缀,截取 s1
或 s2
相应部分均可。
class Solution {
public String longestCommonPrefix(String[] strs) {
//两两比较
String prefix = strs[0];
for(int i = 1; i < strs.length; i++){
//寻找他们的公共前缀
prefix = findCommon(prefix,strs[i]);
}
return prefix;
}
private String findCommon(String s1,String s2){
int i = 0;
while((i < Math.min(s1.length(),s2.length()) && (s1.charAt(i) == s2.charAt(i)))) {
i++;
}
//该区间一定是公共前缀
return s1.substring(0,i);
}
}
1. 两数之和 - 力扣(LeetCode)
解法一:直接暴力解法
class Solution {
public int[] twoSum(int[] nums, int target) {
for(int i=0; i < nums.length; i++){
for(int j = i+1; j
解法二:采用哈希表 key为数组元素,valule为下标
示例1输入:nums = [2,7,11,15], target = 9
如果按照暴力解法当i=2,那就往后遍历,寻找7就是寻找2+?=9,那就可以理解成寻找9-2=?,那我们就可以就可以把遍历过的数全都加入哈希表,当i=2时,计算一下需要寻找几,如果寻找7,只需要查看7元素哈希表中是否存在,如果存在返回i值和哈希表的value,不存在把i下标的值加入哈希表继续向后遍历;
class Solution {
public int[] twoSum(int[] nums, int target) {
Map hash = new HashMap<>();
for(int i = 0; i < nums.length; i++){
//先获取hash表中的值,如果value存在则返回value和当前i
Integer value = hash.get(target - nums[i]);
hash.put(nums[i],i);
if(value != null){
return new int[]{value,i};
}
}
return null;
}
}
1047. 删除字符串中的所有相邻重复项 - 力扣(LeetCode)
本题使用数据结构栈很好写。
我们对输入的字符串进行逐个字符遍历,将遍历到的字符依次尝试压入栈中。在这个过程中,需要进行一个关键的判断:若栈顶元素与当前准备入栈的元素相同,就将栈顶元素弹出,同时放弃将当前元素入栈;若栈为空,或者栈顶元素与准备入栈的元素不相同,那么直接将当前元素入栈即可。
鉴于本题难度较低,为了简化代码实现,我们可以巧妙地使用一个String
类型的变量来模拟栈的行为。
class Solution {
public String removeDuplicates(String s) {
//使用数组模拟栈结构
String stack ="";
for(int i = 0; i < s.length(); i++){
char ch = s.charAt(i);
//如果字符串为空或和准备入栈元素不相等,则直接在后面追加
if(stack.length() > 0 && ch == stack.charAt(stack.length()-1)){
//相等 出栈 substring是右边是开区间,不会包含stack.length()-1
stack = stack.substring(0,stack.length()-1);
} else {
//进栈
stack+= ch;
}
}
return stack;
}
}
429. N 叉树的层序遍历 - 力扣(LeetCode)
在解决经典的层序遍历题目时,我们需要牢牢把握层序遍历的框架。特别要注意的是,必须先对根节点root
进行空值校验。倘若root
为空,那就直接返回,因为若忽略这一步,后续代码执行时极有可能抛出异常。
由于本题要求将元素按照层级分别添加到链表中,所以在遍历队列之前,我们需要先获取当前队列中元素的数量。当这个数量为 1 时,意味着该层级仅有这一个节点,只需将此节点的孩子节点加入队列。接着,把该节点元素添加到链表中。每完成一次层级元素的遍历循环,都要将构建好的链表添加到最终用于返回的链表集合中。
class Solution {
public List> levelOrder(Node root) {
List> ret = new ArrayList<>();
if(root == null) return ret;
Queue queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
//先统计该队列有多少个元素个数
int size= queue.size();
//统计每次的节点信息
List tmp = new ArrayList<>();
for(int i = 0; i< size; i++){
Node node = queue.poll();
//将该层的val值保存到tmp链表中
tmp.add(node.val);
//让所有的孩子入队
for(Node n: node.children){
queue.add(n);
}
}
ret.add(tmp);
}
return ret;
}
}
1046. 最后一块石头的重量 - 力扣(LeetCode)
运用优先级队列来简化解题过程。首先,我们要把数组中的所有元素都添加到优先级队列中。需要特别注意的是,优先级队列默认构建的是小顶堆,即队列头部元素是最小的。但在本题中,我们需要构建大顶堆,也就是让队列头部元素为最大的。通过 new PriorityQueue<>(Collections.reverseOrder()) 建大堆
从优先级队列中取出元素进行操作。当队列中只剩下一个元素时,说明已经到了最终状态,此时直接返回该元素即可。如果经过一系列操作后,队列中没有元素了,也就是成功跳出了循环,那就按照题目的逻辑返回相应结果。
根据题目条件x<=y,在每次从队列中取出元素进行比较和处理时,我们要确保 y
先被取出,也就是 y
在 x
之前被处理,这样才能保证后续操作符合题目的逻辑要求。
class Solution {
public int lastStoneWeight(int[] stones) {
//必须建大堆,优先级队列默认是建小堆
PriorityQueue queue = new PriorityQueue<>(Collections.reverseOrder());
//将数组所有元素加入队列
for(int i: stones){
queue.offer(i);
}
while(!queue.isEmpty()){
//如果队列只有一个元素 直接return
if(queue.size() == 1) return queue.poll();
//根据题目y>=x 所以y要先接受队列中的元素
int y = queue.poll();
int x = queue.poll();
if(x!=y){
queue.add(y-x);
}
}
return 0;
}
}
1137. 第 N 个泰波那契数 - 力扣(LeetCode)
可能有些网友还没有了解过动态规划,这道题是一个很基础的动态规划题目,这种类型题一般都是按模版写,本题的动态规划流程:先创建一个dp表(一维数组或二维数组),根据状态转移方程把dp表填满,然后根据题目返回n下标中的元素
动态规划模版(括号内为本题步骤):
dp
表的含义( T[i]位置中所表示的含义就是状态
)。返回结果:根据问题要求输出最终解(返回 dp[n]
的值)。
class Solution {
public int tribonacci(int n) {
//处理边界情况
if (n == 0) return 0;
if (n == 1 || n == 2) return 1;
//创建dp表 因为要返回第n个下标处的值,所以必须new一个n+1,
int[] dp = new int[n + 1];
//初始化
dp[0] = 0;
dp[1] = dp[2] = 1;
//根据状态转移方程填表
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
}
return dp[n];
}
}
采用滚动数组进行优化,降低空间复杂度;
class Solution {
public int tribonacci(int n) {
//处理边界情况
if(n == 0) return 0;
if(n == 1 || n == 2) return 1;
//创建dp表 因为要返回第n个下标处的值,所以必须new一个n+1,
int a = 0, b = 1, c = 1, d = 0;
for(int i = 3; i <= n;i++){
d = a + b + c;
a = b;b = c; c = d;
}
return d;
}
}
733. 图像渲染 - 力扣(LeetCode)
经典的FlooFill算法题,FlooFill中文就是洪水灌溉,大概就是找出性质相同的联通块。
这种题一般有两种方式解决,DFS深度优先遍历,BFS广度优先遍历
首先对比目标颜色和初始目标的颜色是否相等,如果相等按照题目要求直接返回原数组,
接着就套用做这一类题的模板,队列中存放的是和初始目标元素相邻的元素下标,要修改的元素下班标和初始元素下标的差别只有行加一或列加一的区别,所以定义一个向量数组,把差值存放进去,遍历向量数组,分别相加差值然后满足条件的入队就好
首先,比较目标颜色与初始目标的颜色是否一致。如果两者相等,直接返回原数组即可。
如果颜色不同,则按照以下步骤进行处理:
初始化队列:创建一个队列,用于存储与初始目标元素相邻且需要修改的元素下标。
定义方向向量:为了简化相邻元素的访问,可以定义一个方向向量数组,例如 directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] ,分别表示上、下、左、右四个方向的偏移量。
接着将初始目标元素的下标入队。
当队列不为空时,取出队首元素的下标 (i, j) 。
遍历方向向量数组,计算相邻元素的下标 (x, y) = (i + di, j + dj) 。
检查 (x, y) 是否在数组边界内,并且其颜色与初始目标颜色相同。如果满足条件,则将其颜色修改为目标颜色,并将其下标入队。
返回结果:当队列为空时,所有符合条件的区域都已填充完毕,返回修改后的数组。
class Solution {
//定义了一个向量数组
int[] dx = new int[]{0, 0, -1, 1};
int[] dy = new int[]{-1, 1, 0, 0};
public int[][] floodFill(int[][] image, int sr, int sc, int color) {
int prev = image[sr][sc];
//如果初始像素与目标颜色相等
if (prev == color) return image;
//队列中存放的是要修改元素的下标
Queue queue = new LinkedList<>();
queue.add(new int[]{sr, sc});
int lenX = image.length, lenY = image[0].length;
while (!queue.isEmpty()) {
int[] tmp = queue.poll();
int a = tmp[0], b = tmp[1];
image[a][b] = color;
for (int i = 0; i < 4; i++) {
//每一个元素相当于原始下标 只是行,列加一或者行加一,定义一个向量数组存放,上下左右要修改的下标差值
int x = a + dx[i];
int y = b + dy[i];
//判断边界情况,不能越界,并且颜色和目标颜色不等
if (x >= 0 && x < lenX && y >= 0 && y < lenY && image[x][y] == prev) {
queue.add(new int[]{x, y});
}
}
}
return image;
}
}
1926. 迷宫中离入口最近的出口 - 力扣(LeetCode)
这是一道边权相等的最短路径问题,解题思路是从起点开始进行一次广度优先搜索(BFS)。
首先,从起点出发进行层次遍历。由于图中各边的权值相同,所有路径在搜索过程中将以相同的步长同步扩展。为了确保每个节点仅被访问一次,需要定义一个数组,用来记录已访问过的元素。
在BFS的执行过程中,每当完成一层的遍历,路径长度(即步数)就增加一个单位。当某条路径首次到达目标节点时,由于边权均相等,各条路径在搜索过程中是以相同的步长同步向外扩展的。所以这条路径必然是最短路径。此时,算法终止,当前遍历的层数即为所求的最短路径长度。
class Solution {
//初始化一个向量数组
int[] dx = new int[]{0, 0, -1, 1};
int[] dy = new int[]{1, -1, 0, 0};
public int nearestExit(char[][] maze, int[] entrance) {
//创建队列进行广搜
Queue queue = new LinkedList<>();
//创建数组 记录元素是否遍历过
int m = maze.length, n = maze[0].length;
boolean[][] b = new boolean[m][n];
//将开始位置进入队列
queue.add(entrance);
b[entrance[0]][entrance[1]] = true;
//记录遍历的层数 遍历的层数就是最短路径
int count = 0;
while (!queue.isEmpty()) {
//每次都要把该层元素遍历完
int size = queue.size();
count++;
for (int i = 0; i < size; i++) {
//将元素出队,并把相邻位置符合条件的入队
int[] t = queue.poll();
for (int j = 0; j < 4; j++) {
//计算下标位置
int x = t[0] + dx[j];
int y = t[1] + dy[j];
//判断条件
if (x >= 0 && x < m && y >= 0 && y < n && b[x][y] == false && maze[x][y] == '.') {
if (x == 0 || x == m - 1 || y == 0 || y == n - 1) return count;
queue.add(new int[]{x, y});
b[x][y] = true;
}
}
}
}
return -1;
}
}
542. 01 矩阵 - 力扣(LeetCode)
在路径搜索问题中,单源最短路径和多源最短路径是两个常见的类型。以上题迷宫问题为例属于单源最短路径问题。
本题属于多源最短路径问题,与单源最短路径问题的区别在于:
单源最短路径问题:只有一个源点(起点),目标是找到从该源点到图中所有其他节点的最短路径。
多源最短路径问题:存在多个源点(起点),目标是找到从这些源点到图中其他节点的最短路径。
解决本题这类多源最短路径问题,依然采用广度优先搜索(BFS)算法。具体做法是,将所有的源点合并视为一个“超级源点”,从而把多源最短路径问题转化为单源最短路径问题。通过引入这个超级源点,简化了原本复杂的路径选择过程。本题的解题思路融合了多源BFS算法和“正难则反”的策略。
超级源点思想:将所有源点视为一个虚拟的超级源点,将多源最短路径问题转化为单一的单源最短路径问题。
正难则反思想:将问题从“从多个源点出发寻找最短路径”转换为“从目标点出发向外扩展,更新最短路径”。我们将矩阵中的0作为源点,从0开始向外扩展搜索。一旦遇到值为1的点,就立即将其最短路径更新到对应的位置。如果将1作为源点,当找到0时,我们将难以确定应将最短路径值填充到何处。
多源BFS的具体步骤分为两大步:
1. 把所有起点都加入到队列中。
2. 从队列中的起点开始,一层一层地向外扩展搜索。
实现时,我们首先创建一个与原矩阵 mat 大小相同的二维数组 dist ,用于记录每个位置的最短路径。初始化时,如果 mat[i][j] 的值为0,那么 dist[i][j] 初始化为0;如果 mat[i][j] 不为0,则 dist[i][j] 初始化为-1。
这里 dist 数组下标的元素有两层含义:当值为0时,表示该位置到自身的最短路径为0,同时也意味着该元素已经被访问过;当值为-1时,表示该元素尚未被访问。
与单源最短路径算法不同,在多源BFS中,我们不需要记录遍历的层次,因为 dist 数组的值本身就代表了层次信息。在遍历 dist[i][j] 的上下左右相邻位置时,只要该位置不越界且其值为-1,那么它的最短路径就是 dist[i][j] + 1 。此外,这里也不需要记录队列中的元素数量以及每次需要出队的元素个数,因为每次只需要取出当前元素的上下左右相邻元素进行处理即可。
class Solution {
int[] dx = new int[]{0,0,-1,1};
int[] dy = new int[]{-1,1,0,0};
public int[][] updateMatrix(int[][] mat) {
int m = mat.length, n =mat[0].length;
int[][] dist = new int[m][n];
Queue queue = new LinkedList<>();
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(mat[i][j] == 0){
//将所有0元素下标加入队列 并初始化dist相应位置为0
dist[i][j] = 0;
queue.add(new int[]{i,j});
}else{
//如果不为0初始化为-1,-1表示没有访问过
dist[i][j] = -1;
}
}
}
while(!queue.isEmpty()){
int[] tmp = queue.poll();
for(int i = 0; i < 4; i++){
int x = tmp[0] + dx[i];
int y = tmp[1] + dy[i];
//将满足条件的加入到队列中
if(x >= 0 && x < m && y >= 0 && y < n && dist[x][y] == -1){
//这里的最短距离就是当前源点最短距离+1
dist[x][y] = dist[tmp[0]][tmp[1]] + 1;
//将元素加入队列 准备下一次扩展
queue.add(new int[]{x,y});
}
}
}
return dist;
}
}
207. 课程表 - 力扣(LeetCode)
这道题的核心在于判断给定的课程依赖关系是否构成有向无环图(即图具有方向且不存在环)。
判断的依据是:若拓扑排序结束后,所有节点的入度均为 0,那就表明图中不存在环返回true,进而意味着可以完成所有课程,如果有环返回false。
拓扑排序,是在有向无环图场景下,确定各项任务执行先后顺序的一种算法。值得注意的是,当存在多个入度为 0 的节点时,它们的选取顺序可以任意,因此拓扑排序的结果可能不唯一。并且,如果在拓扑排序完成后,图中仍存在入度不为 0 的节点,那么该图必然是有环图,因为如果环中的顶点没有入度为0的顶点作为起点,因此无法进入队列被处理,导致它们的入度始终无法降为1。
假设存在以下有环图(环外节点 D
指向环内节点 A
):
D → A → B → C → A
A:2
(来自 D
和 C
),B:1
,C:1
,D:0
。D
:D
出队,A
的入度减为 1。A
、B
、C
的入度仍 ≥1,无法进入队列。A:1
、B:1
、C:1
),判定有环拓扑排序的具体步骤如下:
在实现拓扑排序时,我们可以借助队列,通过广度优先搜索(BFS)的方式来完成:
此外,还有一个关键步骤是构建图的数据结构。常见的建图方式有使用链表和哈希表两种,在本题中,我选择采用哈希表来进行建图。
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
//创建数组 统计每一个顶点的入度
int[] n = new int[numCourses];
//创建邻接表 表示图
Map> edges = new HashMap<>();
//建图 将给到的信息存储到图中
for(int i = 0; i < prerequisites.length; i++){
int a = prerequisites[i][0]; //当前课程
int b = prerequisites[i][1]; //先修课程
// 如果邻接表中还没有先修课程,则初始化一个空列表
if(!edges.containsKey(b)){
edges.put(b, new ArrayList<>());
}
// 将当前课程添加到先修课程的邻接表中
edges.get(b).add(a);
//a的入度加一
n[a]++;
}
// 初始化队列,将所有入度为0的课程加入队列
Queue queue = new LinkedList<>();
for(int i = 0; i < numCourses; i++){
//注意int[i]中存放的是点入度的数量而不是表示某个点,i才是表示点
if(n[i] == 0){
queue.add(i);
}
}
// 进行拓扑排序:通过BFS遍历图
while(!queue.isEmpty()){
int t = queue.poll(); // 取出当前课程
//让t的所有连接点的入度-1
//由于可能edags.get(t)并没有连接任何边,会抛异常,所以使用方法如果没有边返回一个空的list 结束循环
for(int i : edges.getOrDefault(t,new ArrayList<>())){
n[i]--;
// 如果邻接课程的入度为0,加入队列
if(n[i] == 0) queue.add(i);
}
}
//判断是否有环 如果入度数组都为0则无环,否则有环
for(int i = 0; i < numCourses; i++){
if(n[i] != 0) return false;
}
return true;
}
}