【Leetcode】剑指offer(第2版)刷题笔记|简单(2)|python|C++

本文总结了《剑指Offer》中的经典算法题,包括二叉树层序遍历、平衡二叉树判断、对称二叉树检测、数组中重复数字查找、连续子数组最大和等题目,提供多种解题思路,如递归、双指针、二分查找等。

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

剑指offer(第2版)刷题笔记|Python|C++|简单

刷题传送门

  1. 从上到下打印二叉树II(32-II)

【思路】

  • 如果是在题目[从上到下打印二叉树I(32-I)]中,所有的数值都打印在一个容器vector中,只需要维护一个队列,对二叉树进行层序遍历即可。
  • 问题的关键在于怎样将不同层的数值打印在不同的容器中
    根据题解(层序BFS遍历)中的思想,用当前队列的长度来控制这一层的循环次数
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int> > res;
        if(!root) return res;
        queue<TreeNode*> q;
        q.push(root);
        while(!q.empty()){
            vector<int> ans;
            int times = q.size();
            while(times--){
                TreeNode *cur = q.front();
                ans.push_back(cur->val);
                q.pop();
                if(cur->left) q.push(cur->left);
                if(cur->right) q.push(cur->right);
            }
            res.push_back(ans);
        }
        return res;
    }
};
class Solution:
    def levelOrder(self, root: TreeNode) -> List[List[int]]:
        if not root: return []
        res , q = [] , collections.deque()
        q.append(root)
        while q:
            tmp = []
            for _ in range(len(q)):
                node = q.popleft()
                tmp.append(node.val)
                if node.left:q.append(node.left)
                if node.right:q.append(node.right)
            res.append(tmp)
        return res
  1. 平衡二叉树(55-II)

【思路】与树相关的题目自然会联想到利用递归进行求解,一棵二叉树是平衡的当且仅当其左右子树都是平衡的,而且左右子树的深度不超过1——所以关键在于实现一个计算树的最大深度的子函数。

class Solution {
    int depth(TreeNode* root){
        if(!root) return 0;
        return max(depth(root->left),depth(root->right))+1;
    }
public:
    bool isBalanced(TreeNode* root) {
        if(!root) return true;
        return isBalanced(root->left)&&isBalanced(root->right)&&abs(depth(root->left)-depth(root->right)) <= 1;
    }
};

K神题解指路
在自己思考的时候,脑子里关于递归的想法并没有明确区分是【先序】还是【后序】;上述方法实质是先序遍历,依次计算子树的深度并由此判断是否平衡——要遍历所有的节点并且会产生重复计算。

后序遍历+剪枝:采用后序遍历,自底向上计算每个节点的深度的同时,对是否平衡进行判断。

  • 递归终止条件:过当前遍历节点为空,说明越界,返回0作为其深度值;
  • 深度值计算:在未明确该树一定非平衡时,其深度值为左右子树深度的最大值+1,否则返回-1表示非平衡
  • 判断+剪枝:
    如果左右子树中出现了-1的返回值,直接返回false
class Solution:
    def isBalanced(self, root: TreeNode) -> bool:
        def recur(root):
            if not root:return 0
            left = recur(root.left)
            if left == -1:return -1
            right = recur(root.right)
            if right == -1:return -1
            return max(left,right)+1 if abs(left-right)<=1 else -1
        return recur(root)!=-1
  1. 对称的二叉树(28)

【思路】
本来想用递归进行求解,但是只想到按照层次进行遍历的:维护一个队列进行层序遍历,逐层判断每一层的数值元素是不是关于中心对称的。

p.s. 但是在求解的过程中发现按照层序对元素进行判断无法明确节点是左子节点还是右子节点,因此空指针用-1来代替;为了让列表非空的判断不至于称为死循环,在压入子节点的操作之前先对该节点是否为空节点进行了判断。

鉴于奇奇怪怪的测试用例,比如说空节点也可以有子节点,所以对于一个空节点的定义为:既没有左右子节点且该节点的值为-1

class Solution:
    def isSymmetric(self, root: TreeNode) -> bool:
        if not root or not root.left and not root.right:return True
        elif not root.left or not root.right:return False
        q = collections.deque([root.left,root.right])
        while q:
            l = len(q)
            for i in range(l):
                if q[i].val != q[l-1-i].val:return False
                if q[i].val != -1 or q[i].left or q[i].right:
                    q.append(q[i].left if q[i].left else TreeNode(-1))
                    q.append(q[i].right if q[i].right else TreeNode(-1))
            while l:
                q.popleft()
                l -= 1
        return True

K神题解】递归做法
对于一棵二叉树,其对称位置的两个节点L和R应该满足:

  • L与R的值相等
  • L的左子树与R的右子树相同
  • L的右子树和R的左子树相同

如果明确了要用递归来求解问题,那么关键在于确定递归的终止条件和递归过程:

  • 终止条件:
    如果对应位置的两个节点的均为空节点,则返回true;
    如果对应位置的两个节点中只有一个为空或均非空但是值不相等,则返回false;
    其他情况下返回true
  • 递归条件:
    对于每对给定的节点L和R,递归地判断L->leftR->rightL->rightR->left是否对称
class Solution:
    def isSymmetric(self, root: TreeNode) -> bool:
        def cur(L,R):
            if not L and not R:return True
            elif not L or not R or L.val != R.val:return False
            return cur(L.left,R.right) and cur(L.right,R.left)
        return cur(root.left,root.right) if root else True
class Solution {
    bool cur(TreeNode* l,TreeNode* r){
        if(!l && !r) return true;
        else if(!l || !r ||l->val != r->val) return false;
        return cur(l->right,r->left)&&cur(l->left,r->right);
    }
public:
    bool isSymmetric(TreeNode* root) {
        if(!root) return true;
        return cur(root->left,root->right);
    }
};

数组

因为本篇笔记是按照数据结构进行划分的,在数组标题下的题目涉及到以下关键词:

  • 哈希
  • 二分法
  • 双指针
  • 分治与递归
  • 动态规划
  1. 数组中重复的数字(03)

【思路】

  • 对数组中的元素挨个遍历,可以将元素放进set中,如果当前的元素已存在于集合set中则输出;或者将元素放在哈希表中记录当前遍历的次数,当前元素的值大于1则输出。
  • 还有一种思路,就是将原数组进行排序,再按序判断相邻是否出现了值相同的数字。
class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        set<int> st;
        for(auto num : nums){
            if(st.find(num) != st.end())
                return num;
            else st.insert(num);
        }
        return 0;
    }
};
class Solution:
    def findRepeatNumber(self, nums: List[int]) -> int:
        dict = {}
        for num in nums:
            if num not in dict:
                dict[num] = 1
            else:
                return num
        return 0
#在py中还可以使用list来代替集合的作用
#使用关键字in进行元素是否在集合内的判断
#或者就使用py中的set对象,利用其无序互异性

【一个巧妙的题解思路】-原地交换

  • 基本思想:因为长度为n的数组中所有数字都在[0,n-1]的数值范围内,所以数组元素的索引和数值是一对多的关系。
  • 基本操作:通过遍历数组并交换元素,使得元素的索引和数值可以一一对应(nums[i]=i)
    在这里插入图片描述
class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        for(int i = 0;i<nums.size();i++){
            if(nums[i] == i) continue;
            else if(nums[nums[i]] == nums[i]) return nums[i];
            else{
                int tmp = nums[i];
                nums[i] = nums[tmp];
                nums[tmp] = tmp; 
            }
        }
        return -1;
    }
};
class Solution:
    def findRepeatNumber(self, nums: List[int]) -> int:
        i = 0
        while i < len(nums):
            if nums[i] == i:
                i += 1
                continue
            elif nums[nums[i]] == nums[i]:
                return nums[i]
            else:
                nums[nums[i]],nums[i] = nums[i],nums[nums[i]]
        return -1
'''
备注:在python中交换数值是可以使用平行赋值,其本质上,a,b = c,d,是先暂存元组(c,d)再从左至右将元素赋值给a和b;
因此上面代码中的平行赋值不能变动顺序。
'''
  1. 和为s的两个数字(57)

【思路】

  • 暴力统计,两层循环,找到结果就输出
  • 双指针,因为数组已经是增序排序好的,维护一个首指针和尾指针,根据计算结果和目标结果的大小关系来移动左右指针
//双指针
//计算值>目标值,则移动左指针;反之移动右指针
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        int i = 0,j = nums.size()-1;
        vector<int> res(2,-1);
        while(i<j){
            int tmp = nums[i]+nums[j];
            if(tmp == target){
                res[0] = nums[i];
                res[1] = nums[j];
                return res;
            }
            else if(tmp < target) i++;
            else j--;
        }
        return res;
    }
};
class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        i,j = 0,len(nums)-1
        while i < j:
            val = nums[i]+nums[j]
            if val == target:
                return [nums[i],nums[j]]
            elif val < target: i+=1
            else: j-=1
        return [-1,-1]
  1. 调整数组顺序使奇数位于偶数前面(21)

【思路】

  • 联想到快排中的partition函数实现的思想
  • 使用两个指针分别从数组的首部和尾部向前和向后搜索,首指针找到的第一个偶数和尾指针找到的第一个奇数进行交换,直到两个指针交叉即结束

p.s.

  • 要注意i和j在移动的时候,边界控制
  • 为了防止数组访问越界,循环控制条件要利用短路求值特性,先进行边界控制
class Solution {
public:
    vector<int> exchange(vector<int>& nums) {
        int i = 0,j = nums.size()-1;
        while(i<=j){
            while(i<nums.size() && nums[i] % 2 == 1) i++;
            while(j>=0 && nums[j] % 2 == 0) j--;
            if(i>=j) break;
            int tmp = nums[i];
            nums[i] = nums[j];
            nums[j] = tmp;
        }
        return nums;//原地修改
    }
};
class Solution:
    def exchange(self, nums: List[int]) -> List[int]:
        i,j = 0,len(nums)-1
        while i<=j:
            while i<len(nums) and nums[i] % 2 == 1: i+=1
            while j>=0 and nums[j] % 2 == 0: j-=1
            if i >= j: break
            nums[i],nums[j] = nums[j],nums[i]
        return nums

【题解思路】
1.快慢指针

  • 使用快指针fast查找下一个奇数所在的位置,以fast遍历完整个数组作为循环控制条件
  • 使用慢指针slow维护下一个奇数应该存放的位置
  • fast搜索到奇数之后就将其存储在slow所指向的位置,并移动slow和fast指针

p.s.注意事项

  • 可以使用位运算来代替取余运算判断奇偶数
class Solution {
public:
    vector<int> exchange(vector<int>& nums) {
        int fast = 0,slow = 0;
        while(fast < nums.size()){
            if(nums[fast] & 1){
                swap(nums[slow],nums[fast]);
                slow++;
            }
            fast++;
        }
        return nums;
    }
};

【题解思路】
2.sort()自定义排序[下方评论区]

  • 最后实现的结果是奇数在前半部分,偶数在后半部分
  • 也就等价与在排序时奇数的优先级比偶数的优先级更高
class Solution {
public:
    vector<int> exchange(vector<int>& nums) {
        sort(nums.begin(),nums.end(),[](int a,int b){return a%2 > b%2;});
        return nums;
    }
};
  1. 连续子数组的最大和(42)

这个是之前刷【动态规划】专题时经常碰到的动规中最经典的题型,以下给出动态规划的代码,详细分析可以见K神题解,动规的解题思路关键在于:

  • 定义好状态和初始状态的值,确定返回的值所对应的状态
  • 写出状态转移方程
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        vector<int> dp;
        int res  = nums[0];
        dp.push_back(nums[0]);
        for(int i = 1;i<nums.size();i++){
            dp.push_back(max(nums[i],dp[i-1]+nums[i]));
            res = max(res,dp[i]);
        }
        return res;
    }
};
#使用一个辅助数组进行动态规划
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        dp = [nums[0]]
        for i in range(1,len(nums)):
            dp.append(max(nums[i],nums[i]+dp[i-1]))
        return max(dp)
#原地修改进行动态规划
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        for i in range(1,len(nums)):
            nums[i] += max(0,nums[i-1])
        return max(nums)
  1. 在排序数组中查找数字I(53-I)

【思路】
就采用最朴实的思路,对数组内的所有元素进行遍历判断,维护一个计数器统计目标值target出现的次数。

因为给定的数组本身就是有序的,所以可以进行剪枝操作,当目标值已经大于当前元素值的时候就可以退出遍历。

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int res = 0;
        for(auto num : nums){
            if(target <num) break;
            else if(target == num) res++;
        }
        return res;
    }
};

【二分查找-K神题解

  • 核心要义:对于有序数组中的搜索问题,常常思考使用二分法进行解决
    在这里插入图片描述
    使用两个指针left和right分别指向目标数值的左、右区间,对区间长度计算就得到所有目标数值的总数。
    无封装版本:对左指针和右指针分别调用一次二分方法进行查找,计算区间的中点值m:
    ->如果nums[m]<target,则target应该在闭区间[m+1,j],执行i=m+1
    ->如果nums[m]>target,则target应该在闭区间[i,m-1],执行j=m-1
    ->如果nums[m]=target,查找右端点的时候应该去[m+1,j]区间,查找左端点的时候应该去[i,m-1]区间
    ->查找完右边界后,nums[j]一定指向的是最右边的目标值target,可以对此判断target是否在数组中存在。
    有封装版本:思路上还是需要进行两次二分查找的代码,可以把二分查找的过程封装成一个函数,为了保证调用的一致性,将分别查找左右指针转换成对target和target-1这两个数值的右边界分别进行查找。
    在这里插入图片描述
class Solution:
    def search(self, nums: [int], target: int) -> int:
        def helper(tar):
            i, j = 0, len(nums) - 1
            while i <= j:
                m = (i + j) // 2
                if nums[m] <= tar: i = m + 1
                else: j = m - 1
            return i
        return helper(target) - helper(target - 1)


  1. 旋转数组的最小数字(11)

【基本思路】将一个递增(非严格)的有序数组进行旋转之后。整个数组会呈现先递增后递减的趋势,也可能只有递增或递减的情况(当数组未旋转或完全逆序时),关键点即在于对转折点的位置进行判断,那个转折点指向的就是最小数值。

①第一次尝试:

  • 指针从i=1到i=len(nums)-2开始遍历,每次都判断该元素和左右相邻的两个元素是否满足递增的关系,不满足则退出遍历,可以根据最后指针停留的情况得到转折点所在的位置;
  • 指针停在i=1,说明未进入循环,数组是递减的;
  • 指针停在i=len(nums)-1,说明循环完全遍历完了,数组是完全递增的;
  • 其他情况,i+1所指向的位置即为转折点所在
  • 在进行循环之前对数组长度为1和2的特殊情况先进行了判断

②第二次尝试:原因是因为发现i=1的时候不仅是数组可能完全递减,还存在其他情况,因此主逻辑不改变,判断逻辑调整为如下:

  • 指针停在i=len(nums)-1,说明循环完全遍历完了,数组是完全递增的,返回首位置元素;
  • 其他情况,针对nums[i-1],nums[i],nums[i+1]这三个元素进行最小值比较,再输出其中最小的那一个
class Solution:
    def minArray(self, numbers: List[int]) -> int:
        if len(numbers) == 1:return numbers[0]
        if len(numbers) == 2:return min(numbers[0],numbers[1])
        i = 1
        while i<=len(numbers)-2:
            if numbers[i]>=numbers[i-1] and numbers[i]<=numbers[i+1]:
                i += 1
            else: break
        if i == len(numbers)-1: return numbers[0]
        else: return min(min(numbers[i-1],numbers[i]),numbers[i+1])

【二分法-K神题解
因为本质上是要找到转折点,前面的思路是遍历+剪枝,对于排序数组的查找问题都可以考虑用二分法将线性复杂度降为对数复杂度。但是本菜鸟没有想到中间元素应该怎么和左右两指针进行比较,从而决定下一步的查找区间。
后来看到题解里的一句话,觉得是是思路的关键突破——左排序数组中的元素始终不小于右排序数组中的任何一个元素
在这里插入图片描述

class Solution {
public:
    int minArray(vector<int>& numbers) {
        int i = 0,j = numbers.size()-1;
        while(i <= j){
            int m = (i+j) / 2;
            if(numbers[m] < numbers[j]) j = m;
            else if(numbers[m]>numbers[j]) i = m+1;
            else j -= 1;
        }
        return numbers[i];
    }
};
  1. 扑克牌中的顺子(61)

【基本思路】目前只能想到简单模拟的思路,将数组先排序,然后用计数器k初始化为0,表示当前可以“出错”(某一个牌不在顺子里)的次数,如果碰到0就可以将计数器加一,一旦计数器的值非负就说明已经不能组成顺子了。
结果在提交代码的时候,发现按照我这种模拟的思路需要考虑很多边界条件而且某些测试用例还过不了,看到题解中的思路——max-min<5即可构成顺子.

class Solution {
public:
    bool isStraight(vector<int>& nums) {
        int count = 0;
        sort(nums.begin(),nums.end());
        for(int i = 0;i<nums.size()-1;i++){
            if(nums[i] == 0) count++;
            else if(nums[i] == nums[i+1]) return false;
        }
        if(nums[4] - nums[count] <5) return true;
        else return false;
    }
};
  1. 0~n中缺失的数字(53-II)

【基本思路】

  1. 因为数组本身是递增排序好的,可以直接对数组进行遍历,哪个地方出现了断链可以直接判断出来。
    p.s. 第一次提交时[1]这个测试用例未通过,因为我默认如果整个循环遍历完了则返回nums末尾的元素+1,但也可能是首元素位置不为0,需要返回0.
  2. 看到题目的标签中有【二分法】,于是自己尝试了一下二分的思想——
    ①关键是要找到不连贯的那个点
    ②如果对于一个长度为n-1的数组,每个数组都是从0开始按顺序排列的自然数,那么下标和数值应该是一一对应的,即nums[i] == i
//C++实现循环遍历判断
class Solution {
public:
    int missingNumber(vector<int>& nums) {
        for(int i = 0;i<nums.size()-1;i++)
            if(nums[i+1]-nums[i]!=1) return nums[i]+1;
        return nums[0] ==0?nums[nums.size()-1]+1:0;
    }
};
#python实现二分查找法
class Solution:
    def missingNumber(self, nums: List[int]) -> int:
        i,j = 0,len(nums)-1
        while i>=0 and j<len(nums) and i<=j:
            m = (i+j) // 2
            if nums[m] == m: i=m+1
            elif nums[m] > m: j=m-1
        return i

数学

  1. 圆圈中最后剩下的数字(62)

【思路】

  • 过程模拟:将所有数字装在容器(如vector)内,然后使用循环控制每次删除第m个数字,直到容器内只剩下一个数字
  • 脑子里有一些关于模运算的雏形,但是不知道怎么应对下标的动态变化

【题解思路】
递归/迭代解决约瑟夫环问题,分解子问题

  • 把长度为n的序列每次删除第m个元素,最后剩下的元素下标记为f(n,m),那么试图用f(n-1,m)来推得最后的问题的答案
  • 如果记x=f(n-1,m),x的含义是从当前下标向后移动m+1个元素,得到的下标对应的元素就是所求的答案
  • 用下标来定义答案,既可以应对下标的动态变化问题,也可以使用模运算处理循环的问题
  • 递归式子为:f(n,m)=[m+f(n-1,m)] % n,详细推导见上述链接的第一个评论区

p.s.递归与迭代

  • 递归法:就是要找到问题和子问题之间的递归方程,定义好递归结束调节,自上至下让程序自己调用递归栈即可;
  • 迭代法:是自己先找到最小子问题的解,自下至上按照递归方程不断更新当前解,直到问题的规模增长到题意的要求为止。
//递归解法
class Solution {
    int f(int n,int m){
        if(n == 1) return 0;
        int x = f(n-1,m);
        return (m+x)%n;
    }
public:
    int lastRemaining(int n, int m) {
        return f(n,m);
    }
};

//迭代解法
class Solution {
public:
    int lastRemaining(int n, int m) {
        int res = 0;
        for(int i = 1;i<=n;i++)
            res = (res + m) % i;
        return res;
    }
};
  1. 不用加减乘除做加法(65)

【本题是照着题解敲出来的】

  • 因为不能使用四则运算,故只能从位运算、逻辑运算符中进行选择;
  • 关键在于找到加法运算中的关键元素与位运算的逻辑关系→本位和 = 两数值对应位置的与运算;进位和 = 两数值对应位置的异或运算
  • 采取的计算逻辑是用a暂存本位和,用b暂存进位和,对a和b反复使用上述逻辑进行累加,直到进位和为0,则此时本文和就是最终的结果。
    p.s.故不要忘了计算进位和之后还需要左移一位
class Solution {
public:
    int add(int a, int b) {
        while(b){
            int c = (unsigned int)(a & b)<<1;
            a ^= b;
            b = c;
        }
        return a;
    }
};
  1. 顺时针打印矩阵(29)

【基本思路】能想到元素的打印应该是遵照某个顺序和规律对下标进行变化:把矩阵遍历的轨迹画了出来,也依次列出了每个元素的下标元组对(i,j)去找其中变化的规律。
发现整个遍历的顺序遵照先右再下再左最后上的顺序进行循环,且每次都是在下标碰到边界的时候发生变化。
p.s. 在测试用例的时候,发现前面的思路还有一个漏洞,就是需要记录原矩阵中已经访问过的元素位置;当访问到已经遍历过的位置或下标越界的时候都需要改变遍历方向。

class Solution:
    def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
        dirr = [[0,1],[1,0],[0,-1],[-1,0]]
        d , count = 0 , 0 #方向指针和元素计数器
        row = len(matrix)
        col = len(matrix[0]) if row else 0
        res = [] #返回的结果

        sumval = row * col
        if sumval == 0:return res

        i , j = 0,0 #初始下标
        flag = [[False for y in range(col)] for x in range(row)]
        while count < sumval:
            res.append(matrix[i][j])
            flag[i][j] = True
            tmpi = i + dirr[d][0]
            tmpj = j + dirr[d][1]
            if tmpi <0 or tmpi >=row or tmpj<0 or tmpj >= col or flag[tmpi][tmpj]:
                d = (d+1) % 4
            i += dirr[d][0]
            j += dirr[d][1]
            count += 1
        return res

链表、栈和队列

  1. 两个链表的第一个公共节点(52)

【思路】-题解

  • 脑子里面有“双指针”三个字,但是不知道从何处开始推导…
  • 基本思路就是指针1先从表A开始遍历再开始遍历表B,指针2则先从表B开始遍历再开始遍历表A,则两个指针必然会相遇——要么在公共节点,要么同为null指针

在这里插入图片描述
p.s.题解里的代码写的极其优雅!!

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode *pa = headA,*pb = headB;
        while(pa != pb){
            if(pa) pa = pa->next;
            else pa = headB;

            if(pb) pb = pb->next;
            else pb = headA;
        }
        return pa;
    }
};
class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        A,B = headA,headB
        while A != B:
            A = A.next if A else headB
            B = B.next if B else headA
        return A
  1. 删除链表的节点

【思路】
除了待删除节点在链表中的特殊位置(头部和尾部)的情况,其余时候关键在于找到待删除节点的前向节点p,然后执行p->next = p->next->next即可。
遍历+判断:因为默认每一个节点的值都不重复,在遍历的过程中维护一个前向指针即可。
p.s.我以为这个算是[暴力解法]了,没想到居然还是属于双指针解法

//遍历+判断
class Solution {
public:
    ListNode* deleteNode(ListNode* head, int val) {
        if(head->val == val) return head->next;
        ListNode *pre = head,*cur = head->next;
        while(cur){
            if(cur->val == val){
                pre->next = cur->next;
                break;
            }
            pre = cur;
            cur = cur->next;
        }
        return head;
    }
};
class Solution:
    def deleteNode(self, head: ListNode, val: int) -> ListNode:
        if head.val == val:return head.next
        pre,cur = head,head.next
        while cur:
            if cur.val == val:
                pre.next = cur.next
                break
            pre = cur
            cur = cur.next
        return head
  1. 包含min函数的栈(30)

【原始思路】
一开始想着底层用vector维护一个栈的结构,用一个数值维护最小的数值,但是这个不能保证最小数值的动态更新。

K神题解
①核心思路:用一个完整的栈A维护元素后进先出的特性,保证pop()操作和push()操作的正确性;再维护一个非严格递减栈B,保证此栈的栈顶始终是最大的元素。

②正确性说明:因为min()操作只需要返回当前栈内最小的元素,因此小于当前栈B栈顶元素的数值不需要压入栈中,其元素是否弹出也不影响min()操作返回的结果。

//放一个我一直过不了大数的代码,先把问题留在这儿...
class MinStack {
stack<long long> st1,st2;
public:
    /** initialize your data structure here. */
    MinStack() {
        while(!st1.empty())
            st1.pop();
        while(!st2.empty())
            st2.pop();
    }
    
    void push(int x) {
        st1.push(long(x));
        if(st2.empty()|| long(x)<=long(st2.top())) 
            st2.push(long(x));
    }
    
    void pop() {
        if(!st2.empty()&&long(st1.top()) == long(st2.top())){
            st2.pop();
            st1.pop();
        }
    }
    
    int top() {
        if(!st1.empty())
            return st1.top();
        else return -1;
    }
    
    int min() {
        if(!st2.empty())
            return st2.top();
        else return -1;
    }
};

字符串

  1. 第一个只出现一次的字符(50)

【思路】
遍历+哈希表:

  • 使用一个哈希表维护每一个字符出现的次数(或者维护一个标志位,代表其是否只在字符串中出现一次)
  • 然后再从字符串开头开始依次进行判断,输出第一个只出现一次得到字符
class Solution {
public:
    char firstUniqChar(string s) {
        map<char,bool> mp;
        for(auto c : s){
            if (mp.find(c) == mp.end())
                mp[c] = true;
            else mp[c] = false;
        }
        for(auto c: s)
            if(mp[c]) return c;
        return ' ';
    }
};
class Solution:
    def firstUniqChar(self, s: str) -> str:
        dic = {}
        for c in s:
            if c in dic:
                dic[c] = False
            else: dic[c] = True
        for c in s:
            if dic[c]: return c
        return ' '

【题解思路及其优化】

优化思路与具体实现机制简要汇总如下:

  1. 前文的思路需要对字符串进行两次遍历,第二次可以直接对建立好的哈希表进行遍历,在字符串长度较大时可以很大程度地节省时间开销:
    √ 方法一:使用哈希表存储索引而非计数,第二次遍历哈希表中最小的数值,其对应的键即为结果
    √ 方法二:使用有序哈希表的结构,则第二次只需要对哈希表进行遍历判断即可
  2. 使用队列来找到第一个不重复的字符
  • 除了使用哈希映射维护每个字符是否值出现一次以外,还使用一个存储着(字符,首次出现索引)二元数对的队列
  • 如果当前遍历的字符是首次出现,则不仅对哈希表处理,还要将(c,index[c])压入队尾
  • 如果当前遍历的字符非首次出现,除了要对哈希表的值进行改动以外,还要将队列中存储的所有不满足情况的二元对都弹出——延迟删除
  • 最后队首的结果(c,index[c])中的c就是待返回的结果,若队列为空则返回空格字符
//使用哈希表存储索引值
class Solution {
public:
    char firstUniqChar(string s) {
        unordered_map<char,int> pos;
        int n = s.size();
        for(int i = 0;i<n;i++){
            if(pos.count(s[i]))
                pos[s[i]] = -1;
            else pos[s[i]] = i;
        }
        int first = n;
        for(auto [_,index]:pos){
            if(index != -1 && index < first)
                first = index;
        }
        return first == n? ' ':s[first];
    }
};
#使用有序哈希表,注意在py3.6之后字典默认是有序的
class Solution:
    def firstUniqChar(self, s: str) -> str:
        dic = {}
        for c in s:
            dic[c] = not c in dic
        for k,v in dic.items():
            if v: return k
        return ' '
  1. 翻转单词顺序(58-I)

【思路】整体上来说思路还是很清晰的,就是以空格作为分隔界限,将一个字符串以字符串数组的形式组织起来,最后按逆序将每个字符串按照一个单词一个空格的形式串联起来得到返回结果。
p.s. 为了学习大佬比较优雅的代码方式,我直接看了题解

  1. 双指针
  • 从字符串的末尾开始遍历,用两个指针维护一个单词的左右索引边界
  • 每次确定了一个单词的边界,就将这个单词添加到单词列表中
  • 最后将单词列表按照题目要求的形式拼接起来
class Solution:
    def reverseWords(self, s: str) -> str:
        s = s.strip() # 删除首尾空格
        i = j = len(s) - 1
        res = []
        while i >= 0:
            while i >= 0 and s[i] != ' ': i -= 1 # 搜索首个空格
            res.append(s[i + 1: j + 1]) # 添加单词
            while s[i] == ' ': i -= 1 # 跳过单词间空格
            j = i # j 指向下个单词的尾字符
        return ' '.join(res) # 拼接并返回

  1. 库函数——调用字符串分割的函数并进行列表倒序
class Solution:
    def reverseWords(self, s: str) -> str:
        return ' '.join(s.strip().split()[::-1])
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值