Leetcode刷题总结(六)17 - 19 -- dfs

本文深入剖析了LeetCode中的三道经典题目:电话号码的字母组合(DFS搜索),四数之和(排序+双指针),以及删除链表倒数第N个节点(后向前遍历)。通过实例讲解了DFS搜索策略,比较了DFS与BFS,以及链表操作技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(一)LeetCode17:电话号码的字母组合

本题是一个非常经典的dfs问题,暴搜问题。
对于dfs问题考虑不清楚的时候,可以画一个树,把情况都枚举一下:(本题号码映射问题的树如下,不过这是树的部分)
在这里插入图片描述
画出这棵树之后,我们要考虑的问题就是要将其转化为代码:
首先,在递归的时候,我们要存下来方案是什么 – 方案即为路径 string path;
还需要存下来当前是第几位,int u。
还有一个问题,如何将数字对应成多个字母,这里有一个小技巧,就是开一个数组。

class Solution {
public:
    vector<string> res; // 最后的答案
    string strs[10] = { // 这里为什么不需要hash表,因为数字正好可以对应相应的字母
        "", "", "abc", "def", "ghi",
        "jkl", "mno", "pqrs", "tuv", "wxyz"
    };

    vector<string> letterCombinations(string digits) {
        if(digits.empty()) return res; // 判断为空的情况
        dfs(digits, 0, ""); // 开始从第0位起
        return res;
    }
    
    void dfs(string& digits, int u, string path) { // 处理目标,当前是第几位,路径
        cout << '*' << path << endl;
        if(u == digits.size()) res.push_back(path); // 如果u已经到最后一位了,就在答案当中加入我们的方案
        // 否则遍历当前的数组可以取哪一些字符
        else {
            for(auto& c : strs[digits[u] - '0']) // 这个循环的代码很神奇
                dfs(digits, u + 1, path + c); 
                // 要不要恢复现场,要看path到底会不会变,如果变了,那就一定要恢复现场到原状态,本题中path是没有变的,所以不需要恢复现场!!!
        }
    }
};

】我们发现path不是变了吗,为什么说没变呢?我们这里指的没变是在回溯的时候并没有变,去继续深搜的时候只是函数的实参传递,相当于传递了给了自己一个新的内容,原内容并没有变,即被压入栈中的内容并没有变,只是在栈顶开辟了一块新的空间,并向其中加入了新的值!

现在我们正式地了解一下dfs暴搜(深度优先搜索),与之相对的有bfs(宽度优先搜索),我们可以与其做一个对比:
首先dfs和bsf都可以对整个空间进行遍历,搜索的结构都是像一棵树一样搜索,但搜索的顺序是不一样的:

这是一棵树
在这里插入图片描述

①. dfs是尽量往深了搜,当搜索到叶节点的时候,就会往回回溯,回溯过程中遇到子节点有叶子节点的就也会往下搜,当碰到叶子节点就又往回回溯!(往最里走,回去的时候也不会一回就回到开始的头部了,而是边回去边看是否能继续往下走,只有确定当前这个点所有的路都走不了的时候,才会往回退一步!!!)
— — 总的来说:dfs是一个执着的人

②. bfs是一层一层搜的,它可以同时看很多条路,只有当一层的所有点都搜完后,才会搜下一层。每次只搜一层,但最终也是所有层都会搜索完
— — 总的来说:bfs是一个眼观六路耳听八方的人

dfs和bfs对比

方式数据结构空间特性
DFSstack 栈O(n)不具有最短路
BFSqueue 队列O(2 ^ n)最短路

了解这两种方式内部的数据结构有什么作用?
我们可以用栈来模拟dfs的搜索过程(这是不用递归的优化)

为什么DFS存n个节点就可以,而BFS要存2^n个?
dfs往下搜的时候只需要记录这条路径上的所有点就可以了;而bfs要把整个一层的所有节点都存下来,它存的是指数级别的。
但dfs还是有优点的,“最短路”,假若每条线段的长度都为1(一个图的所有边都为1的时候),那么第一次搜到的的就是最近的点!!!
空间上BFS不具有优势,但DFS不具有最短性(问最小距离,最小步数,最少操作几次…)。什么实时候体现的较为明显:
在这里插入图片描述
dfs:1 - > 2 - > 3
bfs:1 - > 3

dfs正式】dfs是较为难以理解的,其中有两个概念:
①. 回溯
②. 剪枝:把一些不合法的情况提前判断,下面的子树就不用再搜了
dfs详解:dfs算法详解.

(二)LeetCode18:四数之和

本题与之前的15,16题思路非常类型,都是典型的排序 + 双指针,但本题又添加了一个维度,使得我们也看清了一些问题:

最外层的所谓固定的数的循环应该什么时候才会中止,我们是否应该用固定的nums.size(),nums.size() - 1里中止其的执行?在里面的循环已经不再符合遍历条件时,还应继续执行外部循环吗?

下面我们采用的方式的确是在最外层循环用固定数值来控制:i - > nums.size() 和 j - > nums.size() - 1。所以当里面的循环已经不再符合条件时,还是会继续外部循环!!!(最内部的循环是通过相对条件来控制的:k < r;
所以这里还是三个点固定,一个点在动,有且只能是一个点在动)

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
    if (nums.empty() || nums.size() < 4) return {}; // 这种情况要特判一下,否则用下面代码执行可能会发生溢出
    sort(nums.begin(), nums.end());
    vector<vector<int>> res;
    for (int i = 0; i < nums.size(); i ++) { // 循环的中止条件为啥为nums.size(z)
        if(i && nums[i] == nums[i - 1]) continue;
        cout << i << endl;
       for(int j = i + 1; j < nums.size() - 1; j ++) { // 循环中止条件为啥为nums.size()
           if(j > i + 1 && nums[j] == nums[j - 1]) continue;
           cout << j << endl;
           for(int k = j + 1, r = nums.size() - 1; k < r ; k ++) { 
           // k < r是最终的控制条件,它是把门,由于i, j, k的值都会逐次加1的,所以一定有绝对正确的大小关系,而k和r是相对关系,必须控制好。
               if(k > j + 1 && nums[k] == nums[k - 1]) continue;
               while(r - 1 > k && nums[i] + nums[j] + nums[k] + nums[r - 1] >= target) r --; 
               // 为什么要加r - 1 > k的限制条件?如果不加,r一直减减就会发生重复情况,一定要限制一下循环的步伐
               if(nums[i] + nums[j] - target == - nums[k] - nums[r]) res.push_back({nums[i], nums[j], nums[k], nums[r]}); // 这里要注意一下,由于lc的数据增强了,会发生溢出的问题,所以要将条件处理一下 !
           }
       }
    }
        return res;
    }
};

具体的去重思路和双指针遍历思路已经在之前的 “三数之和” 解释过了,这里不再讨论:Leetcode刷题总结(五)14 - 16

(三)LeetCode19:删除链表的倒数第 N 个结点
一般链表的遍历都是从前往后遍历的,但这里却是要从后往前,怎么办?
1、解决本题常见的思路就是获取该链表的总长度,然后遍历找到倒数第n+1个点,再执行删除。
2、链表元素删除时,注意要先记住被删除节点的前一个链表!!!

我们就以本题作为突破口,来开启链表之旅:
①. 对于链表题,头节点可能会变的情况(比如本题的头节点就可能会被删),就要建立一个虚拟头节点,这个虚拟头节点是肯定不会被删的。建立虚拟头节点两步走:
在这里插入图片描述

②.
在这里插入图片描述
从图中看出,删除倒数第n个点的核心就是找到倒数第n + 1个点。
②. 求链表长度,怎么说: p != nullptr(循环的终止条件)
在这里插入图片描述

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // 看清楚:建立虚拟头节点的方式
        auto dummy = new ListNode(-1); // 这里很聪明,使用了auto作为数据类型接收器
        dummy -> next = head;
        int len = 0;
        for(auto p = dummy; p != nullptr; p = p -> next) len ++;  // 链表的总长度怎么求?
        cout << len;
        // 知道了长度之后,就可以开始执行删除了
        auto p = dummy; // 临时存一下虚拟头节点
        for(int i = 0; i < len - (n + 1); i ++) p = p -> next;  // 跳到倒数第n+1个点
        // 为什么截止次数为len - n - 1:因为这是控制我们要跳几次的,所以用 总长度 - 倒数位数 即可
        // 另外,这里算长度时,虚拟头节点也是计算在内的!!! 
        
        p -> next = p -> next -> next;
        return dummy -> next; // 这里也要注意:返回时要返回dummy -> next,因为head也有可能会被删除
    }
};

③. 最后返回的“头节点”该怎么表示:
return dummy -> next; // 因为头节点本身也是会被删除的,所以这里依然用虚拟头节点来表示!!!
④. 关于内存分配问题,一般情况不需要回收链表的节点,但如果需要,怎么办?
回收内存时,要逐个回收,delete [] p; // 删除一个节点后,再删除下一个,当然,在删除之前要记住当前这个节点,否则就找不到下一个节点了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值