基础算法题6

基础算法题专栏

目录

 岛屿的最大面积

单词接龙

地图中的最高点

火星词典

有效三角形的个数

将x减到0的最小操作数

 X的平方根

出自身意外数组的乘积

只出现一次的数字II

 外观数列


 岛屿的最大面积

695. 岛屿的最大面积 - 力扣(LeetCode)

这道题与基础算法题4中的“岛屿数量”问题在思路上基本一致。二者的核心差异在于:在“岛屿数量”问题中,每执行一次广度优先搜索(BFS),就将岛屿的数量加1,以此来统计岛屿的总数;而在本题中,每进行一次BFS操作,则是更新当前所遍历到的岛屿的最大面积。具体来说,在本题的BFS过程中,只需额外记录一下所遍历到的陆地单元格的数量,通过不断比较这些数量,就能确定并更新最大的岛屿面积。

class Solution {
    // 定义方向数组,用于表示上下左右四个方向的偏移量
    int[] dx = new int[]{0,0,-1,1};
    int[] dy = new int[]{1,-1,0,0};
    int m,n;
    // 用于标记每个位置是否已经访问过的二维布尔数组
    boolean[][] flag ; 
    public int maxAreaOfIsland(int[][] grid) {
        m = grid.length;n = grid[0].length;
        // 初始化标记数组 flag,大小为 m 行 n 列,所有元素初始值为 false,表示都未访问过
        flag = new boolean[m][n];
        
        int ret = 0;
        for(int i=0;i<m;i++){
            for(int j=0; j<n;j++){
                // 如果当前位置未被访问过且值为 1(表示是陆地)
                if(!flag[i][j] && grid[i][j] == 1){
                    // 标记当前位置已访问
                    flag[i][j] = true;
                     // 调用 bfs 方法计算当前岛屿的面积,并更新最大岛屿面积 ret
                    ret = Math.max(ret,bfs(grid,i,j));
                }
            }
        }
        return ret;
    }
    public int bfs(int[][] grid,int i,int j){
        // 创建一个队列,用于广度优先搜索,队列中存储的是位置信息
        Queue<int[]> q = new LinkedList<>();
        q.offer(new int[]{i,j});
        int ret = 0;
        // 当队列不为空时,进行广度优先搜索
        while(!q.isEmpty()){
            int[] t = q.poll();
            //只要可以入队的都是满足条件的 所以岛屿数量直接加1
            ret++;
            int a = t[0], b = t[1];
            for(int k=0; k<4; k++){
                int x = a+dx[k];
                int y = b+dy[k];
                // 如果相邻位置在二维数组范围内,未被访问过且值为 1(表示是陆地)
                if(x>=0 && x<m && y>=0 && y<n &&!flag[x][y] && grid[x][y]==1){
                    flag[x][y] = true;
                    // 将相邻位置加入队列,以便继续搜索
                    q.offer(new int[]{x,y});
                }
            }
        }
        return ret;
    }
}

单词接龙

127. 单词接龙 - 力扣(LeetCode)
和基础算法题4几乎是一样的,只是在原基础算法题4里,需要处理的字符集限定为四个特定字符。而如今,题目的要求出现了重要变化,字符集拓展为所有小写字符。这一改变意味着,在解题过程中,需对之前的算法逻辑进行调整。在遍历并改变基因序列时,要针对每一位的字符,将其循环替换为所有可能的小写字母即可。

class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        // 将单词列表转换为哈希集合以提高查找效率
        Set<String> hash = new HashSet<>();
        for(String t : wordList) {
            hash.add(t);
        }
         // 记录已访问过的单词
        Set<String> vis = new HashSet<>(); 
        // 如果目标单词不在字典中,直接返回0
        if(!hash.contains(endWord)) return 0;
        Queue<String> q = new LinkedList<>();
        q.offer(beginWord);
        vis.add(beginWord);
        // 初始化转换步数为1(包含起始单词)
        int ret = 1;
        while(!q.isEmpty()){
            ret++; // 完成一层处理增加步数
            int size = q.size();
             // 处理当前层的所有节点
            while(size-- != 0){
                String s =  q.poll();                           
                char[] tmp = s.toCharArray();
                // 变换当前单词的每一个字母
                for(int i = 0; i < tmp.length; i++){
                    //记录当前字母,为了后续遍历完之后还原字符串
                    char t = tmp[i];
                    // 尝试用a-z替换当前字母
                    for(char j='a'; j <= 'z'; j++){
                        tmp[i] = j;
                        String newGene = new String(tmp);
                        // 检查变换后的单词是否有效
                        if(!vis.contains(newGene) && hash.contains(newGene)){
                            //如果变换后的单词等于目标单词直接返回
                            if(endWord.equals(newGene)) return ret;
                            // 将有效变换加入队列并标记为已访问
                            q.offer(newGene);
                            vis.add(newGene);
                        }
                    }
                    tmp[i] = t; //还原字符串
                }
            }
        }
        //遍历完以后没找到咋返回0
        return 0;
    }
}

地图中的最高点

1765. 地图中的最高点 - 力扣(LeetCode)

在进行广度优先搜索(BFS)处理水域相关问题时,首先将所有水域的元素加入队列,并将其值初始化为0。同时,把非水域的元素标记为 -1,这既代表它们是陆地,也表明这些陆地尚未被访问。
 完成初始化后,开始执行BFS算法。在搜索过程中,一旦遇到尚未访问过的元素,便依据该元素在扩展前所处位置的下标,将其值更新为扩展前元素值加1 。

class Solution {
    //定义四个方向移动偏移量数组
    int[] dx = new int[]{0,0,-1,1};
    int[] dy = new int[]{1,-1,0,0};
    public int[][] highestPeak(int[][] isWater) {
        int m = isWater.length, n = isWater[0].length;
        int[][] height = new int[m][n];
        //初始化返回矩阵,并加水域高度0加入到队列
        Queue<int[]> q = new LinkedList<>();
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                if(isWater[i][j] == 1){
                    height[i][j] = 0;
                    q.add(new int[]{i,j});
                }else {
                    //-1表示为未访问过并且是陆地
                     height[i][j] = -1;   
                }
            }
        }
        //BFS过程
        while(!q.isEmpty()){
            int[] t = q.poll();
            int a = t[0], b = t[1];
            //遍历当前下标处的上下左右四个位置
            for(int i = 0; i < 4; i++){
                int x = a + dx[i];
                int y = b + dy[i];
                //检查下标合法性,并未访问过
                if(x>=0 && x<m && y>=0 && y<n && height[x][y] == -1){
                    //相邻陆地为当前高度+1
                    height[x][y] = height[a][b] + 1;
                    //将新下标加入到队列中,以便下一次扩展
                    q.add(new int[]{x,y});
                 } 
            }
        }
        return height;
    }
}

火星词典

LCR 114. 火星词典 - 力扣(LeetCode)

该题要读懂题,比较相邻的两个单词 words[i] 和 words[j](i < j),找到第一个不同的字符 c1 和 c2(c1 != c2)。这表示 c1 必须排在 c2 前面,即 c1 -> c2。

例如,["wrt", "wrf"] 中 't' -> 'f',因为 't' 和 'f' 是第一个不同的字符。

无效字典序的检测:如果 words[j] 是 words[i] 的前缀(如 "abc" 和 "ab"),则直接返回 "",因为字典序要求更长的单词必须排在更短的单词后面(如果前缀相同)。

使用拓扑排序进行解决,第一步先初始化化入度表都为0

第二步建图,比较所有单词对,找到第一个不同的字符并建立边,如果是新边则增加c2的入度之和,若 words[j] 是 words[i] 的前缀(如 "abc" 和 "ab"),直接返回 "";

第三步拓扑排序:

初始化队列:将所有入度为 0 的字符加入队列。
处理队列:取出字符 ch,加入结果 ret。
遍历 ch 的所有邻居 c,减少其入度;若入度为 0,加入队列

 第四步检查环:若拓扑排序后仍有字符的入度不为 0,说明存在环,返回 ""。

注意存储边关系时使用Set集合是为了保证边不会重复,并且必须在有新边时才更新入度,而且比较不同时只找出第一个不同,不关心后续字符;

class Solution {
    public String alienOrder(String[] words) {
        // 构建图:edges 表示字符之间的边,例如 edges['a'] = {'b', 'c'} 表示 'a' -> 'b' 和 'a' -> 'c'
        Map<Character, Set<Character>> edges = new HashMap<>();
        // 存储每个字符的入度(即有多少字符指向它)
        Map<Character, Integer> in = new HashMap<>();
        // 初始化入度:遍历所有单词的所有字符,初始入度为 0
        for (String s : words) {
            for (char ch : s.toCharArray()) {
                in.put(ch, 0);
            }
        }
        // 建图:比较所有单词对,建立字符之间的顺序关系
        for (int i = 0; i < words.length; i++) {
            for (int j = i + 1; j < words.length; j++) {
                String s1 = words[i]; // 前面的单词
                String s2 = words[j]; // 后面的单词
                int k = 0;
                int n = Math.min(s1.length(), s2.length());
                // 逐个字符比较,找到第一个不同的字符
                for (; k < n; k++) {
                    char c1 = s1.charAt(k), c2 = s2.charAt(k);
                    if (c1 != c2) {
                        // 确保 c1 的边集合已初始化
                        if (!edges.containsKey(c1)) {
                            edges.put(c1, new HashSet<>());
                        }                    
                        // 如果 c1 -> c2 是新边(之前不存在),则增加 c2 的入度
                        if (edges.get(c1).add(c2)) {
                            in.put(c2, in.get(c2) + 1);
                        }
                        break; // 只需处理第一个不同的字符
                    }
                }      
                // 如果 s2 是 s1 的前缀(如 "abc" 和 "ab"),则字典序无效,返回 ""
                if (k == s2.length() && k < s1.length()) {
                    return "";
                }
            }
        }
        // 拓扑排序:将所有入度为 0 的字符加入队列
        Queue<Character> q = new LinkedList<>();
        for (char ch : in.keySet()) {
            if (in.get(ch) == 0) {
                q.offer(ch);
            }
        }
        StringBuilder ret = new StringBuilder(); // 存储拓扑排序结果
        while (!q.isEmpty()) {
            char ch = q.poll();
            ret.append(ch);
            // 如果当前字符没有出边(即 edges 中没有它的记录),跳过
            if (!edges.containsKey(ch)) {
                continue;
            }
            // 遍历所有 ch 指向的字符,减少它们的入度
            for (char c : edges.get(ch)) {
                in.put(c, in.get(c) - 1);
                // 如果入度减为 0,加入队列
                if (in.get(c) == 0) {
                    q.offer(c);
                }
            }
        }
        // 检查是否有环:如果仍有字符的入度不为 0,说明存在环,返回 ""
        for (char ch : in.keySet()) {
            if (in.get(ch) != 0) {
                return "";
            }
        }
        return ret.toString(); 
    }
}

有效三角形的个数

611. 有效三角形的个数 - 力扣(LeetCode)

如果暴力解法就是把所以可能全都列出来,但是决定会超时,针对暴力解法进行优化:

1.暴力解法时,以判断三角形三边关系为例,传统暴力解法是分别比较三次: (a + b > c)、(a + c > b) 以及 (b + c > a)。然而,若对数组进行排序,仅需判断一次,即比较两个较小值的和与最大值的大小关系。这是因为在有序数组中,最大值必然大于任何一个较小值,无需再进行额外比较。

2.对于枚举所有可能情况的暴力解法,优化思路同样基于排序。在排序后,从最大值开始从后向前遍历数组。此时,定义两个指针,left 指向数组开头,right指向当前最大值的前一个位置。当 left 和 right 所指元素之和大于当前最大值时,说明在 left 到 right 这个区间内的所有组合都满足条件。因为若 left 继续向后移动,其与 right 的和只会更大,必然也大于最大值。此时,只需计算这个区间的元素个数,然后将 right向左移动一位继续判断。

当 left 和 right 所指元素之和小于当前最大值时,应将 left`向右移动。这是因为只有这样,left 和 right的和才会逐渐增大,以便继续寻找满足条件的组合。

class Solution {
    public int triangleNumber(int[] nums) {
        int count = 0, len = nums.length;
        Arrays.sort(nums); // 先对数组排序,方便后续双指针操作
        // 从后往前遍历,固定最大的边 nums[i]
        for(int i = len-1; i > 1; i--){
            for(int left=0,right=i-1; left<right; ){
                // 如果 nums[left] + nums[right] > nums[i],说明:
                // 1. nums[left], nums[right], nums[i] 可以构成三角形
                // 2. 由于数组已排序,left 到 right-1 的所有元素与 nums[right]、nums[i] 也能构成三角
                if(nums[left] + nums[right] > nums[i]){
                    count += right-left;
                    right--; // 尝试更小的 nums[right]
                } else {
                    // 如果 nums[left] + nums[right] <= nums[i],说明 nums[left] 太小,
                    // 需要增大 nums[left](左指针右移)
                    left++;
                }
            }
        }
        return count;
    }
}

将x减到0的最小操作数

1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)

按照题目原本的要求直接求解,难度极大。因此,不妨转换思路,采用逆向思维的方法。题目原本是要在左区间找到一个区间加上右区间,使它们元素相加等于x,这种做法困难重重。但从图中看出,除了a和b所在的部分,中间的区间是连续的,并且这个连续区间所有值的总和恰好等于整个区间的总和减去x。

如此一来,问题便转化为找出最长子数组长度,他们的和等于数组总和 sum 减去目标值 x ,而这一问题可以借助滑动窗口算法加以解决。

需要留意的是,题目要求的是最少操作数。在求解总和为 sum - x 的连续区域时,我们实际上要找的是最长的这样一个连续区域,因为只有连续区域最长,左右两侧用于调整的 a 和 b 的数量才会最少。另外,当数组总和小于目标值 x 时,可直接返回结果,因为在这种情况下,无论如何操作都无法使数组总和减少至 x 。

class Solution {
    public int minOperations(int[] nums, int x) {
        int count = -1;  // 记录满足条件的子数组的最大长度(初始化为-1表示未找到)
        int len = nums.length;
        int sum1 = 0;
        for(int i = 0; i < len; i++) sum1 += nums[i]; //求出数组元素和
        // 特殊情况处理:如果x大于总和,直接返回-1(因为无法通过操作使总和减少到x)
        if(x > sum1) return -1; 
        int sum2 = 0;
         // 滑动窗口算法:通过双指针left和right维护窗口
        for(int left=0,right=0; right < len;right++){
            // 右指针向右移动,扩大窗口(增加当前和)
            sum2 += nums[right];
            // 当窗口内和超过目标值(sum1 - x)时,左指针右移缩小窗口
            while(sum2 > sum1 - x){
                sum2-=nums[left++];  // 减去左边界元素并移动左指针
            }
            // 如果窗口内和正好等于目标值,更新最大窗口长度
            if(sum2 == sum1 - x){
                count =Math.max(count,right-left+1);
            }
        }
        // 返回结果:若找到有效子数组,则总长度减去子数组长度即为最小操作数;否则返回-1
        return count == -1 ? -1 : len-count;
    }
}

 X的平方根

69. x 的平方根 - 力扣(LeetCode)

从暴力解法分析,能够发现这道题具有二段性,因此可以直接运用二分查找的思路来解题。具体操作是,持续取中间值进行相乘运算。当运算结果小于或等于目标值时,把左端点更新为当前中间值,这里不能跳过中间值,因为答案可能是向上取整的,当前中间值有可能就是我们要找的结果。而当运算结果大于目标值时,就直接跳过该中间值。此外,要特别注意使用 long类型进行计算,因为使用 int 类型可能会导致溢出问题。

class Solution {
    public int mySqrt(int x) {
        long left = 0,right = x;
        while(left < right){
            // 计算中间值(+1避免死循环,例如x=1时left=0, right=1的情况)
            // 注意:这里(left + right + 1)/2 等效于向上取整,保证区间收缩方向正确
            long mid = left + (right - left + 1) /2 ;
            if(mid <= x/mid) left = mid;
            else right = mid - 1;
        }
        return (int)left; 
    }
}

除自身以外数组的乘积

238. 除自身以外数组的乘积 - 力扣(LeetCode)

这道题与基础算法题 4 中寻找数组的中心下标时采用的两个前缀和解法极为相似。不同之处在于,本题需要处理边界情况。由于通过new操作创建的元素初始值都为 0,因此必须将前缀积数组中第一个元素(下标为 0)之前的积初始化为 1,而不能是 0,否则后续计算结果都将为 0。同理,后缀积数组中最后一个元素也要进行类似处理。最后,将每个下标位置对应的前缀积和后缀积相乘,并将结果存入返回数组中,完成合并操作。

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int len = nums.length;
        // 前缀积数组:prev[i] 表示 nums[0] × nums[1] × ... × nums[i-1]
        int[] f = new int[len];
        // 后缀积数组:latter[i] 表示 nums[i+1] × nums[i+2] × ... × nums[len-1]
        int[] g = new int[len];
        // 初始化边界值
        f[0] = g[len-1] = 1;
        // 预处理:计算前缀积(从左到右)
        for(int i = 1; i < len; i++){
            f[i] = nums[i-1] * f[i - 1]; // 递推公式:prev[i] = prev[i-1] × nums[i-1]
        }
        // 预处理:计算后缀积(从右到左)
        for(int i = len-2; i >= 0; i--){
            g[i] = nums[i + 1] * g[i + 1]; // 递推公式:latter[i] = latter[i+1] × nums[i+1]
        }
        int[] ret = new int[len];
        // 合并结果:ret[i] = 前缀积 × 后缀积
        for(int i=0; i < len; i++){
            ret[i] = f[i] * g[i];
        }
        return ret;
    }
}

只出现一次的数字II

137. 只出现一次的数字 II - 力扣(LeetCode)

利用位运算统计每一位出现1的次数,因为其他数字都出现三次,所以数组每个元素每位的次数总和模3后,剩下的就是唯一数字的比特位;

这里无需纠结 n 的具体数值,因为我们并不需要确切知晓数组中每个元素在第 i 个比特位上出现的次数。即便有两个元素(每个元素有三个相同的,即总共 6 个元素)在第 i 比特位上同时出现,它们的出现次数也无关紧要,因为对 3 取模后结果为 0。我们唯一需要关注的,是仅出现 1 次的那个数字在第 i 比特位上的值。这个唯一出现一次的数字在该比特位的值必然等于总次数对 3 取模的结果,无论这个结果是 0 还是 1。具体而言:

  • 当总次数 % 3 = 0 时,唯一数字在该比特位的值为 0;
  • 当总次数 % 3 = 1 时,唯一数字在该比特位的值为 1。

关键的认知突破点在于:无需计算每个元素的具体出现次数,仅需明确总次数与 3 的余数关系。这一过程就如同使用筛子,对 3 取模的操作会自动滤除所有重复三次的数字。

class Solution {
    public int singleNumber(int[] nums) {
        int ret = 0;
        //统计nums数组中所有元素每一位比特位的和
        for(int i = 0; i< 32; i++){
            int sum = 0; // 统计当前位(第i位)上所有数字的1的个数
              // 遍历数组中的每个数字
            for(int j = 0; j < nums.length; j++){
                // 检查当前数字的第i位是否为1 ,如果结果 != 0 表示第i位是1,则计算器sum+1
                if((nums[j] & (1 << i) )!= 0 ) sum++;
            }
            // 对于出现三次的数字,它们的每一位的和应该是3的倍数
            // 所以sum % 3得到的就是只出现一次的数字在当前位的值(0或1)
            // 将这个值左移i位,然后与ret进行或运算,设置ret的对应位
            ret |= (sum % 3) << i;
        }
        return ret;
    }
}

 外观数列

38. 外观数列 - 力扣(LeetCode)

这道题属于模拟类型的题目。在初始化返回值时,直接将其设为 "1",这是由于序列的第一项必定是 "1"。后续只需进行 n - 1 次解释操作,因此从 1 开始进行模拟。若只需生成第一列(即 n = 1 的情况),直接返回初始的 "1" 即可。

接下来,定义两个双指针用于遍历待解释的字符串。当遇到与前一个字符不同的字符,或者超出待解释字符串的范围时,便可以更新结果字符串。需要注意的是,更新时应先追加字符出现的计数,再追加字符本身。同时,不要忘记更新 left 指针的位置,left 指针始终指向当前正在解释的字符下标。每完成一次解释,都需要将临时生成的解释字符串拷贝回返回字符串,这是因为在解释过程中,临时字符串可能会覆盖掉原本返回字符串中的部分内容。

class Solution {
    public String countAndSay(int n) {
        // 初始字符串为"1",这是外观数列的第一项
        String s = "1";
        // 解释n-1次来得到第n项
        // 例如:n=1直接返回"1",n=2需要解释一次,n=3需要解释两次,以此类推
        for(int i = 1; i < n; i++) {
            StringBuilder tmp = new StringBuilder(); 
            // 使用双指针技术遍历当前字符串
            // left指向当前字符组的起始位置,right用于扩展查找相同字符
            for(int left = 0, right = 0; right < s.length();) {
                // 扩展right指针,直到找到不同的字符或到达字符串末尾
                while(right < s.length() && s.charAt(left) == s.charAt(right))  right++;
                // 将计数和字符添加到新字符串中
                // right-left计算相同字符的数量
                tmp.append("" + (right - left));  // 添加计数
                tmp.append(s.charAt(left));       // 添加字符本身
                // 移动left指针到下一组字符的起始位置
                left = right;
            }     
            // 更新s为新的解释结果,准备下一次迭代
            s = tmp.toString();
        }
        return s;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值