Skip to content

代码随想录 ​

代码随想录:https://www.programmercarl.com/

由于本人参加了代码随想录的 python 组打卡,那么文章笔记的记录会偏向于记录 python 语言


Day 1


1、数组理论基础


  • 数组是 上的相同类型数据的集合。
  • 数组可以方便的

举个字符数组的例子,如图所示:


微信截图_20250108183423

需要注意的两点是:

  • 数组 的。
  • 数组

正是因为 数组在内存空间的地址是连续的,

所以我们在 元素的时候,就难免

例如:删除下标为 3 的 元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示:


微信截图_20250108184133

数组的元素是不能删的,只能覆盖。

要是可以删,留个空位出来?那计算机内部怎么按照下标来找对应的元素?


那么二维数组直接上图:


微信截图_20250108184434

那么二维数组在内存的空间地址是连续的么?

不同编程语言的内存管理是不一样的,以C++为例,在C++中二维数组是连续分布的。

我们来做一个实验,C++ 测试代码如下:

cpp
void test_arr() {
    int array[2][3] = {
		{0, 1, 2},
		{3, 4, 5}
    };
    cout << &array[0][0] << " " << &array[0][1] << " " << &array[0][2] << endl;
    cout << &array[1][0] << " " << &array[1][1] << " " << &array[1][2] << endl;
}

int main() {
    test_arr();
}

测试地址为:

txt
0x7ffee4065820 0x7ffee4065824 0x7ffee4065828
0x7ffee406582c 0x7ffee4065830 0x7ffee4065834

注意地址为16进制,可以看出二维数组地址是连续一条线的。

Ps : 0x7ffee40658200x7ffee4065824 差了一个4,就是4个字节,

因为这是一个 int 型的数组,所以两个相邻数组元素地址差4个字节。

0x7ffee40658280x7ffee406582c 也是差了4个字节,在16进制里 8 + 4 = c,c 就是12。


如图:


微信截图_20250108185110

Ps : 相当于把一个二维的数组拉直了一样,先存了第一行,

然后第一行的最后一个元素紧接着第二行的第一个元素,这样就存成了一条连续的地址

这种可以叫做是 。( 同理列优先存储的方式就是先第一列然后第二列... )



2、704.二分查找


本节题目

对应力扣题目链接:https://leetcode.cn/problems/binary-search/


给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target

写一个函数搜索 nums 中的 target,如果

示例 1 :

txt
输入: nums = [-1,0,3,5,9,12], target = 9   

输出: 4       

解释: 9 出现在 nums 中并且下标为 4

示例 2 :

txt
输入: nums = [-1,0,3,5,9,12], target = 2   

输出: -1        

解释: 2 不存在 nums 中因此返回 -1

tips :

  • 你可以假设 nums 中的所有元素是不重复的。

  • n 将在 [1, 10000] 之间。

  • nums 的每个元素都将在 [-9999, 9999]之间。



查找算法


查找:在一些数据元素中,通过一定的方法找出与给定关键字相同的数据元素的过程。

  • 列表查找( 线性表查找 ):从列表中查找指定元素

    • 输入:列表、待查找元素值
    • 输出:元素下标( 未找到元素时一般返回 None 或者 -1 )
  • 内置列表查找函数 : index()



顺序查找
  • : 也叫线性查找,从列表第一个元素开始,

    顺序进行搜索,直到找到元素或搜索到列表的最后一个元素位置

  • 时间复杂度:O(n)

代码实现:

python
def linear_search(li,val):
    """

    :param li: 传入要查找的序列
    :param val: 传入要查找的值
    :return: 找到返回查找到的值的索引,没找到返回None
    """

    for ind,v in enumerate(li):
        if v == val:
            return ind
    else:
        return None


二分查找
  • : 又叫折半查找,从 的初始候选区 li[0:n]

    开始,通过对待查找的值与候选区的中间值 mid 的比较,可以使候选区减少一半。

  • 时间复杂度:O(log n)
  • 空间复杂度:O(1)

图解算法逻辑:


  • 例如 要在这个有序的 1- 9 的列表查找元素 3 的位置
  • 首先初始化 : left = 0 and right = len(li)-1
微信截图_20250108194743
  • 根据 leftright 算出 mid
  • mid = (left + right) // 2
微信截图_20250108194758
  • 上一步判断 mid > 3
  • 则待查找的元素在 mid 左侧 , 修改 rightmid 左一位
微信截图_20250108194808
  • 根据新的 leftright 重新计算 mid
微信截图_20250108194815
  • 这次 mid 小于待查找的元素,所以说明待查找的元素在 mid 右侧
  • 修改 leftmid 的右一位
微信截图_20250108194823
  • 最后重新计算 mid 时 , 满足了循环退出条件 mid == 待查找元素(val)

  • 此时 mid 的值就是要查找的元素的下标值,return mid 即可

    微信截图_20250108194832

代码实现:

python
def binary_search(li, val):
    """

    :param li: 传入要查找的序列
    :param val: 传入要查找的值
    :return: 返回查找到的值的索引
    """

    left = 0
    right = len(li) - 1
    while left <= right:  # 候选区有值
        # 候选区就是 left在左,right在右,然后夹着的中间的区域
        mid = (left + right) // 2
        if li[mid] == val:
            return mid  # 找到了返回查找元素的索引
        
        elif li[mid] > val:  # 待查找的值在mid左侧
            right = mid - 1
            
        else:  # li[mid] < val 待查找的值在mid右侧
            left = mid + 1

    else: # 没找到return None
        return None


二分法的区间定义

二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。

例如到底是 while(left < right) 还是 while(left <= right)

到底是right = middle 呢,还是要 right = middle - 1 呢?


  • 写二分法,区间的定义一般为两种,左闭右闭即 [left, right],或者左闭右开即 [left, right)

  • 我们定义 target 是在一个在左闭右闭的区间里,也就是 [left, right] (这个很重要非常重要)

Ps : 其实上面图解的那种情况就是左闭右闭的写法


注意点:

  • while (left <= right) 要使用 <= ,因为 left == right 是有意义的,所以使用 <=

  • if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个 nums[middle]

    一定不是 target ,那么接下来要查找的左区间结束下标位置就是 middle - 1


微信截图_20250108231732

(左闭右闭)题目代码:

python
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) - 1  # 定义target在左闭右闭的区间里,[left, right]

        while left <= right:
            middle = left + (right - left) // 2
            
            if nums[middle] > target:
                right = middle - 1  # target在左区间,所以[left, middle - 1]
            elif nums[middle] < target:
                left = middle + 1  # target在右区间,所以[middle + 1, right]
            else:
                return middle  # 数组中找到目标值,直接返回下标
        return -1  # 未找到目标值

定义 target 是在一个在 左闭右开 的区间里,也就是[left, right)

那么二分法的边界处理方式则截然不同。


注意点:

  • while (left < right),这里使用 < ,因为 left == right 在区间 [left, right) 是没有意义的

    • 比如给定一个区间 [1,1) , 左闭取1,右开不取1,那么这个区间定义没有意义,根本没有值
    • 比如给定一个区间是 [1,2) , 也就是上面的 (left < right) , 1<2 , 这样有意义,可以取 1
  • if (nums[middle] > target) right 更新为 middle,因为当前 nums[middle] 不等于 target

    去左区间继续寻找,而寻找区间是左闭右开区间,所以 right 更新为 middle

    即:下一个查询区间不会去比较 nums[middle]


微信截图_20250108232650

(左闭右开)题目代码:

python
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums)  # 定义target在左闭右开的区间里,即:[left, right)

        while left < right:  # 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            middle = left + (right - left) // 2

            if nums[middle] > target:
                right = middle  # target 在左区间,在[left, middle)中
            elif nums[middle] < target:
                left = middle + 1  # target 在右区间,在[middle + 1, right)中
            else:
                return middle  # 数组中找到目标值,直接返回下标
        return -1  # 未找到目标值


题目通过记录:


微信截图_20250108232849


3、27.移除元素


本节题目

对应力扣题目链接:https://leetcode.cn/problems/remove-element/description/


给你一个数组 nums 和一个值 val,你需要 移除所有数值等于 val 的元素,

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

  • 示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。

    你不需要考虑数组中超出新长度后面的元素。

  • 示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5,

    并且 nums 中的前五个元素为 0, 1, 3, 0, 4, 你不需要考虑数组中超出新长度后面的元素。



思路

多余的元素,删掉不就得了?

( 虽然说我们 py 人有列表可以用,列表直接删掉指定元素它就会自动调整好位置 )

但是为了学习的角度,我们当成我们只能用数组,而数组为什么不能直接删?

要知道


暴力解法

这个题目暴力的解法就是两层 for 循环,

  • 一个for循环遍历数组元素
  • 第二个for循环更新数组。

删除过程如下:



暴力解法的时间复杂度是 O(n^2),这道题目暴力解法在 leetcode 上是可以过的。

暴力解法 - 代码如下:

python
class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        i, l = 0, len(nums)
        while i < l:
            if nums[i] == val: # 找到等于目标值的节点
                for j in range(i+1, l): # 移除该元素,并将后面元素向前平移
                    nums[j - 1] = nums[j]
                l -= 1
                i -= 1
            i += 1
        return l


双指针法

双指针法(快慢指针法):

定义快慢指针:

  • 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
  • 慢指针:指向更新 新数组下标的位置

删除过程如下:



双指针 - 代码如下:

python
class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        # 快慢指针
        fast = 0  # 快指针
        slow = 0  # 慢指针
        size = len(nums)
        while fast < size:  # 不加等于是因为,a = size 时,nums[a] 会越界
            # slow 用来收集不等于 val 的值,如果 fast 对应值不等于 val,则把它与 slow 替换
            if nums[fast] != val:
                nums[slow] = nums[fast]
                slow += 1
            fast += 1
        return slow



题目通过记录:


微信截图_20250109030451


4、977.有序数组的平方


本节题目

对应力扣题目链接:https://leetcode.cn/problems/squares-of-a-sorted-array/description/


给你一个按 的整数数组 nums,

返回 每个数字的平方 组成的新数组,要求也按


示例 1:

  • 输入:nums = [-4,-1,0,3,10]
  • 输出:[0,1,9,16,100]
  • 解释:平方后,数组变为 [16,1,0,9,100] ,排序后,数组变为 [0,1,9,16,100]

示例 2:

  • 输入:nums = [-7,-3,2,3,11]
  • 输出:[4,9,9,49,121]
  • Ps : 由给出的示例 nums 可见,序列其实就是单调递增的,但

    负数比正数小,但是负数经过平方后不一定比正数小 ...



暴力排序

最直观的想法,莫过于:每个数平方之后,排个序,代码如下:

python
class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        for i in range(len(nums)):
            nums[i] *= nums[i]
        nums.sort()
        return nums

这个时间复杂度是 O(n + nlogn), 可以说是 O(nlogn) 的时间复杂度,

但为了和下面双指针法算法时间复杂度有鲜明对比,记为 O(n + nlog n)



双指针法

思想:

数组其实是有序的, 只不过负数平方之后可能成为最大数了。

那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。

此时可以考虑双指针法了,i 指向 (左侧) 起始位置,j 指向 (右侧) 终止位置。

定义一个新数组 result,和 原 ( A ) 数组一样的大小,让 k 指向 result 数组终止位置。

  • 如果 A[i] * A[i] < A[j] * A[j]

    说明两边都平方,右边的平方后更大,

    所以把右边平方后的值放到 result 数组 索引为 k 的位置上,且 k--

    那么 result[k--] = A[j] * A[j];

  • 类似地 A[i] * A[i] >= A[j] * A[j] 那么 result[k--] = A[i] * A[i];

如动画所示:



双指针 - 代码如下:

python
class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        l, r, i = 0, len(nums)-1, len(nums)-1
        res = [float('inf')] * len(nums) # 需要提前定义列表,存放结果
        while l <= r:
            if nums[l] ** 2 < nums[r] ** 2: # 左右边界进行对比,找出最大值
                res[i] = nums[r] ** 2
                r -= 1 # 右指针往左移动
            else:
                res[i] = nums[l] ** 2
                l += 1 # 左指针往右移动
            i -= 1 # 存放结果的指针需要往前平移一位
        return res

ps : float('inf') 是生成一个 的数


此时的时间复杂度为 O(n),相对于暴力排序的解法 O(n + nlog n) 还是提升不少的。



题目通过记录:


微信截图_20250109034440


Day 2


1、209.长度最小的子数组


本节题目

对应力扣题目链接 :https://leetcode.cn/problems/minimum-size-subarray-sum/description/


给定一个含有 n 个正整数的数组和一个正整数 s

找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。

如果不存在符合条件的子数组,返回 0


示例:

  • 输入:s = 7, nums = [2,3,1,2,4,3]
  • 输出:2
  • 解释:子数组 [4,3] 是该条件下的长度最小的子数组。

提示:

  • 1 <= target <= 10^9
  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^5


暴力解法

这道题目暴力解法当然是 两个for循环,然后不断的寻找符合条件的子序列,

  • 解释:
    • 外层 for 循环控制起始位置
    • 内层 for 循环控制终止位置
    • 这样把全部情况遍历出来,然后找出符合条件的那个
    • 但注意题目的提示 :1 <= nums.length <= 10^5
    • 我感觉 py 人都别考虑这个了,一看就得超时 ....

时间复杂度很明显是 O(n^2)

这里给出代码,但肯定超时的,可以了解一下逻辑:

python
class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        l = len(nums)
        min_len = float('inf')	# min_len 初始化为无穷大
        
        for i in range(l):	# 控制起始位置
            cur_sum = 0
            for j in range(i, l):	# 控制终止位置
                cur_sum += nums[j]
                if cur_sum >= s: 
                    min_len = min(min_len, j - i + 1)	# 比较 min_len 与 当前长度
                    break
        
        return min_len if min_len != float('inf') else 0

ps : float('inf') 是 生成一个 的数 , 这样第一次 min(min_len, j - i + 1) 它比任何数小



滑动窗口


接下来就开始介绍数组操作中另一个重要的方法:

所谓滑动窗口,

  • 在暴力解法中,是一个 for 循环控制起始位置,一个 for 循环控制终止位置,

    用两个 for 循环 完成了一个不断搜索区间的过程。

那么滑动窗口如何用一个 for 循环来完成这个操作呢 ?

首先要思考 如果用一个 for 循环,那么应该表示 滑动窗口的

如果只用一个for循环来表示 滑动窗口的 起始位置,那么如何遍历剩下的终止位置?

此时难免再次陷入 暴力解法的怪圈 。

所以 只用一个 for 循环,那么这个循环的索引,

既然 for 循环是控制终止位置了,那么起始位置怎么控制呢??

以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程:



最后找到 [4,3] 是最短距离 。

其实从动画中可以发现滑动窗口也可以理解为双指针法的一种!

只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更适合一些。


实现滑动窗口,主要确定如下三点:

  • 窗口内是什么?
  • 如何移动窗口的起始位置?
  • 如何移动窗口的结束位置?

确定问题答案:

  • 窗口就是

  • 窗口的起始位置如何移动:如果当前**** ,窗口就要向前移动了(也就是该缩小了)。

  • 窗口的结束位置如何移动:窗口的结束位置就是 ,也就是 for 循环里的索引。


微信截图_20250110223315

可以发现 :

滑动窗口的精妙之处在于

。从而将 O(n^2) 暴力解法降为 O(n)

滑动窗口 - 代码如下 :

python
class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        
        l = len(nums)	# 数组nums长度
        
        left = 0	# 起始位置
        
        right = 0	# 终止位置
        
        min_len = float('inf')	# 最小长度 ,初始化为无穷大,后续有任何比它小的都会更新
        
        cur_sum = 0 	#当前的累加值
        
        while right < l:	# 当终止位置小于nums的长度就一直循环遍历终止位置
            
            cur_sum += nums[right]
            
            while cur_sum >= s: 	# 当前累加值大于目标值
                
                # 若当前长度是更小的则更新min_len
                min_len = min(min_len, right - left + 1)
                
                # 在改变起始位置之前,不要忘了减掉当前起始位置的值
                cur_sum -= nums[left]
                
                # 改变起始位置,缩小窗口大小
                left += 1
            
            # 遍历终止位置
            right += 1
        
        return min_len if min_len != float('inf') else 0



题目通过记录:


微信截图_20250111175902


2、59.螺旋矩阵II


本节题目

对应力扣题目链接:https://leetcode.cn/problems/spiral-matrix-ii/description/


给定一个正整数 n,生成一个包含 1n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。


示例 1 :

输入: n = 3

输出: [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]


示例 2:

输入:n = 1 输出:[[1]]

提示:

  • 1 <= n <= 20


题解

这道题目可以说在面试中出现频率较高的题目,

本题并不涉及到什么算法,就是 过程,但却十分考察对代码的掌控能力。


要如何画出这个螺旋排列的正方形矩阵呢?

还记得前面的 中有类似的思想,在这种多次循环中我们要坚持

也就是我们需要在一开始就想清楚那个循环过程,

模拟顺时针画矩阵的过程:

  • 填充上行从左到右
  • 填充右列从上到下
  • 填充下行从右到左
  • 填充左列从下到上

由外向内一圈一圈这么画下去。

可以发现这里的边界条件非常多,在一个循环中,如此多的边界条件,

如果不按照固定规则来遍历,那就是 一进循环深似海,从此offer是路人

那么如果按照 的区间定义来画一圈的话就是:


微信截图_20250111165103

这里每一种颜色,代表一条边,我们遍历的长度,

可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。

但是还有一个 关键的点 需要注意:

比如像上图那样,就是 n = 3 的情况 ,那么画出了一个 3 * 3 的正方形矩阵

在画完圈之后,内部会还有一个位置是空缺的,也就是 当

当 n 为 偶数的时候则不会有那个空缺,画完圈则已经填满

n = 3 的情况 ,就画出了一圈,然后空了一个

我们可以很容易的知道一个问题的答案 :

其实就是 n // 2


代码如下:

python
class Solution:
    def generateMatrix(self, n: int) -> List[List[int]]:
        
        # 先生成一个 n x n 的空白正方形矩阵 matrix
        matrix = [[0 for j in range(n)] for i in range(n)]

        start_x = 0 # 画圈起始位置的横坐标

        start_y = 0 # 画圈起始位置的纵坐标

        cur_num = 1 # 当前填充的数值

        offset = 1	# 当前这圈的终止位置偏移量

        for i in range(n//2):
            # 画 n // 2 圈

            # 上边界,从左到右画圈
            # 那么x不变,y向右走
            for j in range(start_y,n-offset):
                # range的左开右闭
                # 那么下标范围就是 0 - (n-1)
                matrix[start_x][j] = cur_num

                # 填充一次后改变填充的数值
                cur_num += 1

            # 右边界,从上到下画圈
            # 那么y不变,x向下走

            for j in range(start_x,n-offset):
                matrix[j][n-offset] = cur_num

                # 填充一次后改变填充的数值
                cur_num += 1

            # 下边界,从右到左画圈
            # 那么x不变,y向左走
            for j in range(n-offset,start_y,-1):
                matrix[n-offset][j] = cur_num

                # 填充一次后改变填充的数值
                cur_num += 1

            # 左边界 , 从下到上画圈
            # 那么y不变,x向上走
            for j in range(n-offset,start_x,-1):
                matrix[j][start_y] = cur_num

                # 填充一次后改变填充的数值
                cur_num += 1

            # 画完一圈后改变起始位置
            start_x += 1
            start_y += 1

            # 改变偏移量
            offset += 1

        # 全部画完后判断 n 的奇偶

        if n % 2 != 0:
            matrix[start_x][start_y] = cur_num

		return matrix



题目通过记录:


微信截图_20250111180007


3、58. 区间和


本节题目

本题为代码随想录后续扩充题目 , 非力扣题目

题目链接 : https://kamacoder.com/problempage.php?pid=1070


给定一个整数数组 Array,请计算该数组在每个指定区间内元素的总和。

  • 输入描述 :

第一行输入为整数数组 Array 的长度 n,接下来 n 行,每行一个整数,表示数组的元素。

随后的输入为需要计算总和的区间,直至文件结束。

  • 输出描述:

输出每个指定区间内元素的总和。

  • 输入示例:
txt
5
1
2
3
4
5
0 1
1 3
  • 输出示例:
txt
3
9
  • 提示信息:
txt
数据范围:
0 < n <= 100000


暴力思路

首先来看本题,我们最直观的想法是什么?

那就是给一个区间,然后 把这个区间的和都累加一遍不就得了,是一道简单得不能再简单的题目。

虽然知道肯定会 TLE , 但是还是写一下暴力的代码:

python
# 数组长度 n
n = int(input())

# 数组
arr = []

# 构建数组
for i in range(n):
    arr.append(int(input()))

# print(arr)

# 接下来是 q 次询问,每次询问给出左区间L 和 右区间R
# 需要输出区间 [L,R] 的 和
# 但是这道题没有询问 q 的输入,那么我们只能不断的接收输入,并且结合try-except

while True:

    try:
        l,r = map(int,input().split())

        print(sum(arr[l:r + 1]))

    except:
        # 接收不到输入报错的时候退出这个死循环
        break

示例运行结果:


微信截图_20250112004311

示例数据量太少,通过完全没有问题,但是提交看看:


微信截图_20250112004529

没有悬念的 TLE , 因为它这个算法一直在做重复的事情,当数据一大时间就爆炸了...

来举一个极端的例子,如果我查询 m 次,每次查询的范围都是从 0n - 1

也就是 m 次 询问,每次都是 0 n-1 , l 永远是 0r 永远是 n-1

这就很暴漏出这个算法的问题了,每次都是同一个区间的和,但是每次都重新计算

那么该算法的时间复杂度是 O(n * m)



前缀和

接下来我们来引入 前缀和,看看 前缀和 如何解决这个问题。

前缀和的思想是 :

( 有点动态规划的味道 ~ ~ )

前缀和 在涉及计算区间和的问题时非常有用 !


前缀和的思路其实很简单,举个例子很容易就懂了。

例如,我们要统计 vec[i] 这个数组上的区间和。

我们先做累加,即 p[i] 表示 下标 0ivec[i] 累加 之和。

如图:


微信截图_20250112005838

如果,我们想统计,在 vec 数组上 下标 2 到下标 5 之间的累加和,

那是不是就用 p[5] - p[1] 就可以了。

为什么呢 ?


微信截图_20250112010059

这不就是我们要求的 下标 2 到下标 5 之间的累加和吗。

再如下图所示:


微信截图_20250112010302

p[5] - p[1] 就是上面的红色的区间部分

而 p 数组是我们之前就计算好的累加和,

所以后面每次求区间和的之后 我们只需要 O(1) 的操作。

: 如果我们要求 区间下标 [2, 5] 的区间和,

那么应该是 p[5] - p[1],而不是 p[5] - p[2]

如果说还记得 高中数学的数列方面的知识的话, 应该记得有一个公式

就是 : S n-m = Sn - Sm-1 这就是上面的思想 ( Sn 是前 n 项的和 )

代码实现:

python
# 数组长度 n
n = int(input())

# 数组
arr = []

# 构建数组
for i in range(n):
    arr.append(int(input()))

# print(arr)

# 构建前缀和数组
sum_arr = [0 for i in range(n)]
sum_arr[0] = arr[0]

for i in range(1,n):
    # 下标为0在前面初始化好,然后从1开始
    sum_arr[i] = sum_arr[i-1] + arr[i]

# print(sum_arr)

# 接下来是 q 次询问,每次询问给出左区间L 和 右区间R
# 需要输出区间 [L,R] 的 和
# 但是这道题没有询问 q 的输入,那么我们只能不断的接收输入,并且结合try-except
while True:

    try:
        l,r = map(int,input().split())

        if l==0:
            print(sum_arr[r])
        else:
            print(sum_arr[r] - sum_arr[l-1])

    except:
        # 接收不到输入报错的时候退出这个死循环
        break



题目通过记录:


微信截图_20250112012345


4、44.开发商购买土地


本节题目

本题为代码随想录后续扩充题目 , 非力扣题目

题目链接 : https://kamacoder.com/problempage.php?pid=1044


【题目描述】

在一个城市区域内,被划分成了n * m 个连续的区块,每个区块都拥有不同的权值,代表着其土地价值。

目前,有两家开发公司,A 公司和 B 公司,希望购买这个城市区域的土地。

现在,需要将这个城市区域的所有区块分配给 A 公司和 B 公司。

然而,由于城市规划的限制,只允许将区域按横向或纵向划分成两个子区域,

而且每个子区域都必须包含一个或多个区块。

为了确保公平竞争,你需要找到一种分配方式,

注意:区块不可再分。

【输入描述】

第一行输入两个正整数,代表 n 和 m。

接下来的 n 行,每行输出 m 个正整数。

输出描述

请输出一个整数,代表两个子区域内土地总价值之间的最小差距。

【输入示例】

txt
3 3
1 2 3
2 1 3
1 2 3

【输出示例】

txt
0

【提示信息】

txt
如果将区域按照如下方式划分:

1 2 | 3
2 1 | 3
1 2 | 3 

两个子区域内土地总价值之间的最小差距可以达到 0。

数据范围:

1 <= n, m <= 100;
n 和 m 不同时为 1。


思路

本题不同于 58.区间和 , 这个变成二维的数据了,如果这个想要暴力求解的话, 应该是 n^3 的时间复杂度

暴力的话即 : 一个 for 枚举分割线, 嵌套 两个 for 去累加区间里的和。

虽然说,本题数据变成了二维的数据,但是因为是用 来分割,并非求

所以用不到 这么高级 , 仔细研读题目可以发现,其实就是一个

思路就是:如果对 sum( 某一行 ) 作为该行的值,那么就变成了一个一维的数据,

同理 如果对 sum( 某一列 ) 作为该列的值,那么也变成了一个一维的,这样的就实现了降维

从而可以退化到前面我们 的思路

分别求出 行的前缀和 & 列的前缀和 后 就可以遍历分割线的位置,

然后收集全部 土地总价值之间的差距 ,然后取里面的最小值就是答案

代码实现:

python
# import pprint

# 第一行输入两个正整数,n和m
n, m = map(int, input().split())

matrix = []

# 构建题目中输入的 n * m 的矩阵
for i in range(n):
    cur_row = [*map(int, input().split())]
    matrix.append(cur_row)

# pprint.pprint(matrix)

# 按行构建前缀和数组
sum_row = [0 for i in range(n)]
sum_row[0] = sum(matrix[0])  # 初始化第一个值为第一行的和

for i in range(1, n):
    sum_row[i] = sum(matrix[i]) + sum_row[i - 1]

# print(sum_row)

# 按列构建前缀和
sum_col = [0 for i in range(m)]
sum_col[0] = sum([row[0] for row in matrix])

for i in range(1, m):
    sum_col[i] = sum([row[i] for row in matrix]) + sum_col[i - 1]

# print(sum_col)

result = []  # 用于存储两土地之差的

# 按行分割的话,两土地之差有
for i in range(n - 1):
    result.append(abs(sum_row[i] - (sum_row[n - 1] - sum_row[i])))

# 按列分割的话,两土地之差有
for i in range(m - 1):
    result.append(abs(sum_col[i] - (sum_col[m - 1] - sum_col[i])))

print(min(result))



  • 题目通过记录:

微信截图_20250112053411


Day 3


1、链表理论基础


关于链表, 你该了解这些 !

什么是链表?

链表是一种通过指针串联在一起的线性结构,

每一个节点由两部分组成,一个是 一个是 (存放指向下一个节点的指针),

最后一个节点的指针域指向 null ( 空指针的意思)。

链表的入口节点 称为链表的头结点也就是 head

如图所示 :


微信截图_20250112172134

链表的类型


  • 单链表

刚刚说的 , 上图 , 就是单链表


  • 双链表

单链表中的指针域只能指向节点的下一个节点 next

双链表:每一个节点有两个指针域,一个指向下一个节点 next ,一个指向上一个节点 prev

双链表 既可以 向前查询 也可以 向后查询。

如图所示:


微信截图_20250112172734
  • 循环链表

循环链表,顾名思义,就是链表首尾相连。

循环链表可以用来解决 约瑟夫环问题

如图所示:


微信截图_20250112172853

链表的存储方式


了解完链表的类型,再来说一说链表在内存中的存储方式。

数组 是在内存中是连续分布的,但是链表 在内存中可


: 很多人会有一个误解 ,python 的 原生数据结构 list 可以随意的

增加元素,它不像 c 的 数组那样, 需要更大的空间就需要定义,

就会让人感觉 它内部是不是 链表 实现的 ?

其实不是,Python 列表本质上是一个

这意味着列表在内存中是以 的存储空间来存储元素的,

并且还有个关键点,它

这就有下列优势:由于内存连续,通过索引访问元素非常高效,时间复杂度为 O(1)

而且与静态数组不同,列表的大小可以动态变化。

而且 , 我们可以由此知道, 拿 python 的列表 来做大量的增删

时间复杂度是 O(n) , 而 链表做增删的时间复杂度是 O(1)


链表是通过指针域的指针链接在内存中各个节点。

所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,

分配机制取决于操作系统的内存管理。

如图所示:


微信截图_20250112173240

这个链表起始节点为 2, 终止节点为 7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。



链表的定义


关于链表的定义,很多同学经常会忘记

这是也是因为平时在刷 leetcode 的时候,链表的节点都默认定义好了,直接用就行了

所以 没有注意到链表的节点是如何定义的。

而在面试的时候,一旦要自己手写链表,就写的错漏百出。

代码随想录上的 C/C++ 定义链表节点方式,如下所示:

c++
// 单链表
struct ListNode {
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
};

python 的定义链表节点的方式,如下所示:

python
class ListNode:
    def __init__(self, val, next=None):
        self.val = val		# 节点上存储的元素		# 数据域
        self.next = next	# 指向下一个节点的指针	# 指针域


链表的操作


Ps : 下列链表的操作的 python 代码基于这个链表的定义:

python
class Node:
    def __init__(self,item):
        self.item = item        # 数据域
        self.next = None        # 指针域

  • 链表的创建

用链表的数据结构来存储已有的数据


  • 1. 头插法

在头指针 head 的位置一个一个插入的方式来创建链表

假如说已有的链表如下:


微信截图_20250113205213

可以看到 头指针 head 现在 指向一个

如果要继续执行下一步头插法操作的话,就应该

python
"""----非可运行代码,仅为演示部分代码---"""
cur_node = Node(element)    # 为当前遍历到的元素创建新节点

微信截图_20250113205231

然后首先 新创建的这个节点需要


微信截图_20250113205258

然后再 指向刚刚新插入的节点


微信截图_20250113205307

头插法的 python 代码如下 :

python
def create_linklist_head(li):
    # 传入一个列表li,使用头插法创建一个数据相同的链表
    
    head = Node(li[0])  # 创建一个头节点,传入列表的第一个元素
    
    for element in li[1:]:
        # 遍历列表第二个及其后面的元素,使用头插法创建链表
        cur_node = Node(element)    # 为当前遍历到的元素创建新节点
        cur_node.next = head        # 先把新节点的 next 指向 head
        head = cur_node             # 然后再更新 head 为刚刚插入的节点

    return head         # 返回头节点

  • 2. 尾插法

在尾指针 tail 的位置一个一个插入的方式创建链表

那么这种方式就需要两个指针

  • 头指针:最后需要 return head , 需要靠 头指针来找到整个链表的数据
  • 尾指针 :需要尾指针指定最后一个节点在哪,用于插入新的节点

微信截图_20250113211105

然后后续过程也跟前面类似,注意顺序就好了,尾节点的 next 先链接上,再更新 tail

尾插法 python 代码如下 :

python
def create_linklist_tail(li):
    # 传入一个列表li,使用尾插法创建一个数据相同的链表
    
    head = tail = Node(li[0])  # 一开始初始化头节点和尾节点都是li[0]
    
    for element in li[1:]:  # 遍历li[1:]
        cur_node = Node(element)    # 为当前遍历到的元素创建新节点
        tail.next = cur_node        # 先让尾节点的next指向cur_node
        tail = cur_node             # 然后更新新的尾节点为刚刚插入的节点

    return head # 返回头节点

知道了头插法和尾插法,下面再提供一个简易的打印链表的方式来查看一下两个方式创建的链表怎么样:

python
# 打印链表

def print_linklist(head):
    # 传入一个链表的头节点,根据这个节点遍历打印这个链表
    count = 1
    while head:
        if head.next != None:
            print(head.item,"-->",end=" ")
            if count % 6 == 0:	# 计数,每一行打印六个后再换行
                print()
        else:
            print(head.item,end=" ")

        head = head.next
        count += 1
    print()

除函数定义外,演示代码如下:

python
# 头插法
lk = create_linklist_head([1,5,4,2,6])
print_linklist(lk)

# 尾插法
lk2 = create_linklist_tail([1,5,4,2,6])
print_linklist(lk2)

运行效果:


微信截图_20250113211843

可以见到 : 头插法创建的链表是 的,尾插法创建的链表是



  • 链表的插入

前面是从无到有创建一个链表,那如果已经有了一个链表,需要插入一个元素到特定位置怎么办?


微信截图_20250113220432

关键还是要注意操作的顺序 :

  1. 为新插入的元素创建一个节点
  2. 把新节点链接到链表正确的位置
  3. 再把 cur_node 链接到新插入的节点

:每次这些顺序问题只要注意 就行了

比如前面的 第三步 , 如果放到前面的话 , cur_nodenext 先链接到

新插入的节点,那么 cur_node 节点后面的节点都会消失,游离在内存里,最后被清理掉 ...


微信截图_20250113220935

链表的插入 python 代码实现:

python
# 链表的插入
def lk_insert(lk,pos,ele):
    """

    :param lk: 要插入的链表
    :param pos: 要插入的位序(下标;索引)
    :param ele: 要插入的元素
    :return: lk
    """

    if pos == 0:
        # 为要插入的元素,创建一个节点
        p = Node(ele)
        p.next = lk
        lk = p
        return lk


    # 为要插入的元素,创建一个节点
    p = Node(ele)

    count = 0 # 位序计数

    cur_node = lk  # 当前遍历到的节点,初始化为 head

    # 需要遍历到要插入的位置的前一个节点 即 pos-1
    while cur_node:
        # 当前没遍历到Null就可以一直往下走
        if count == pos - 1:
            break

        cur_node = cur_node.next

        count += 1

    else:
        # 这里不是通过 break 退出的
        # 就是没找到那个位置
        print("pos out of range")
        return lk

    # 找到位置就可以执行插入操作
    p.next = cur_node.next
    cur_node.next = p

    del cur_node

    return lk

测试代码:

python
# 尾插法
lk2 = create_linklist_tail([1,2,6])
print_linklist(lk2)

# 链表插入
lk2 = lk_insert(lk2,2,99)     # 1. 插入99到下标为2的位置
print_linklist(lk2)         # 打印
lk2 = lk_insert(lk2,4,88)     # 2. 插入88到最后一个位置
print_linklist(lk2)         # 打印
lk2 = lk_insert(lk2,0,33)       # 3.插入33到第一个位置
print_linklist(lk2)         # 打印
lk2 = lk_insert(lk2,99,100)   # 4. 插入到没有的pos
print_linklist(lk2)

运行效果:


微信截图_20250113224817




  • 链表的删除

给定一个链表,删除特定位置的元素


还是注意操作顺序就可以,关键点就是

  1. 找到要删除的节点 del_node 和 要删除的节点的前一个节点 cur_node
  2. 先把 cur_nodenext 链接到 del_nodenext
  3. 然后 del_node 其实就不在这个链表了,可以认为是已经被删除了,但也可以 del del_node

链表的删除 python 代码实现:

python
# 链表删除
def lk_delete(lk,pos):
    """

    :param lk: 要删除的链表
    :param pos: 要删除的位序
    :return: lk
    """

    # 如果要删除的是第一个节点
    if pos == 0:
        lk = lk.next
        return lk

    # 位序计数
    count = 0

    # 遍历游标
    cur_node = lk       # 初始化为头节点

    # 遍历游标找到要插入的位置的前一个位置 , 也就是 pos - 1
    while cur_node:
        # 当没到Null就可以一直往下遍历

        if count == pos - 1:
            break

        cur_node = cur_node.next
        count += 1

    else:
        # 正常退出就是没找到
        print("pos out of range")
        return lk

    # 执行删除操作
    del_node = cur_node.next  # 先记住被删除的节点
    cur_node.next = del_node.next

    del del_node
    del cur_node

    return lk

测试代码:

python
# 尾插法
lk2 = create_linklist_tail([1,2,6,5,4])
print_linklist(lk2)

# 链表删除
lk2 = lk_delete(lk2,2)        # 1. 删除下标为 2 的节点
print_linklist(lk2)             # 打印
lk2 = lk_delete(lk2,3)        # 2. 删除最后一个节点
print_linklist(lk2)             # 打印
lk2 = lk_delete(lk2,0)        # 3.删除第一个节点
print_linklist(lk2)             # 打印
lk2 = lk_delete(lk2,999)      # 4. 传入没有的pos
print_linklist(lk2)             # 打印

运行效果:


微信截图_20250113224827




2、203.移除链表元素


本节题目

对应力扣链接:https://leetcode.cn/problems/remove-linked-list-elements/description/


题意:删除链表中等于给定值 val 的所有节点。


  • 示例 1: 输入:head = [1,2,6,3,4,5,6] , val = 6 输出:[1,2,3,4,5]

  • 示例 2: 输入:head = [], val = 1 输出:[]

  • 示例 3: 输入:head = [7,7,7,7], val = 7 输出:[]



思路

这里以链表 1 4 2 4 来举例,移除元素 4


微信截图_20250114005910

如果使用 C/C++ 编程语言的话,不要忘了还要从内存中删除这两个移除的节点, 清理节点内存之后如图:


微信截图_20250114005957
  • 当然如果使用 javapython 的话就不用手动管理内存了。

还要说明一下,就算使用 C++ 来做 leetcode,如果移除一个节点之后,

没有手动在内存中删除这个节点,leetcode 依然也是可以通过的,

只不过,内存使用的空间大一些而已,但建议依然要养成手动清理内存的习惯。

这种情况下的移除操作,就是让节点 next 指针直接指向下下一个节点就可以了

那么因为单链表的特殊性,只能指向下一个节点,

刚刚删除的是链表的中第二个,和第四个节点,

那么

这里就涉及如下链表操作的两种方式 :

  • 直接使用原来的链表来进行删除操作。
  • 设置一个虚拟头结点在进行删除操作。

  • 第一种操作:直接使用原来的链表来进行移除。

微信截图_20250114010314

移除头结点和移除其他节点的操作是不一样的,

因为链表的其他节点都是通过前一个节点来移除当前节点,而

所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,

这样就从链表中移除了一个头结点。


微信截图_20250114010433

c++ 依然别忘将原头结点从内存中删掉。

这样移除了一个头结点,是不是发现,

在单链表中移除头结点 和 移除其他节点的操作方式是不一样,

其实在写代码的时候也会发现,需要


那么就到第二种操作的好处了

  • 第二种操作:设计一个虚拟头节点

这样原链表的所有节点就都可以按照统一的方式进行移除了。

来看看如何设置一个虚拟头。依然还是在这个链表中,移除元素1。


微信截图_20250114010747

这样每个结点都有一个 ,这样的话所有的节点的操作就都可以统一了

最后呢在题目中,return 头结点的时候,

别忘了 return dummyNode->next; , 这才是新的头结点

因为 return head 这种情况,你是不是没想到可能删除的节点有可能是 head ??


代码如下:

python
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:

        # 创建一个虚拟头节点
        dummy_node = ListNode()

        # 虚拟头节点挂在 head 前面
        dummy_node.next = head

        # 遍历游标节点
        cur_node = dummy_node       # 初始化为最前面的虚拟头节点

        while cur_node:

            while cur_node.next != None and cur_node.next.val == val:
                # 如果cur_node 的下一个节点的值就是val的话
                # 循环地不断删除到不是val为止

                # 删除操作
                cur_node.next = cur_node.next.next

            cur_node = cur_node.next    # 向下遍历


        # dummy_node.next 才是头节点
        # 因为删除的节点也可能是 head
        return dummy_node.next


  • 题目通过记录

微信截图_20250114012249


3、707.设计链表


本节题目

对应力扣题目链接:https://leetcode.cn/problems/design-linked-list/description/


题意:

在链表类中实现这些功能:

  • get(index) :获取链表中第 index 个节点的值。如果索引无效,则返回-1。

  • addAtHead(val) :在链表的第一个元素之前添加一个值为 val 的节点。

    插入后,新节点将成为链表的第一个节点。

  • addAtTail(val) :将值为 val 的节点追加到链表的最后一个元素。

  • addAtIndex(index,val) :在链表中的第 index 个节点之前添加值为 val 的节点。

    如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,

    则不会插入节点。如果 index 小于0,则在头部插入节点。

  • deleteAtIndex(index) :如果索引 index 有效,则删除链表中的第 index 个节点。


微信截图_20250114050300


题解


总结题意:

这道题目设计链表的五个接口:

  • 获取链表第 index 个节点的数值
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
  • 在链表第 index 个节点前面插入一个节点
  • 删除链表的第 index 个节点

可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目

只要练熟了这道题,基本上就可以算掌握了链表的基本操作

链表操作的两种方式 :

  1. 直接使用原来的链表来进行操作。
  2. 设置一个虚拟头结点在进行操作。

只给出虚拟头节点的代码,因为这是很好的方式统一了很多操作:

代码如下:

python
# 单链表法 - 设计链表

# 定义链表节点类
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

# 定义链表类
class MyLinkedList:
    def __init__(self):
        # 虚拟头节点
        self.dummy_head = ListNode()
        self.size = 0

    # 获取链表 第 index 个节点的数值
    # 注意 index 从 0 开始算
    def get(self, index):
        # index 的合法性
        if index < 0 or index >= self.size:
            return -1  # 不合法

        # 初始化为首元结点,因为如果index是0的话直接就是首元结点的值了
        cur = self.dummy_head.next

        for i in range(index):
            cur = cur.next

        return cur.val

    # 在链表的最前面插入一个节点
    def addAtHead(self, val):

        # 这样先执行等号右边再执行左边也不会丢失链表,也是正确的顺序
        self.dummy_head.next = ListNode(val=val, next=self.dummy_head.next)
        self.size += 1

    # 在链表的最后面插入一个节点
    def addAtTail(self, val):
        cur = self.dummy_head
        while cur.next:
            # 当cur.next不为Null一直遍历,最后一个节点的next为Null
            cur = cur.next

        # 新创建的节点的next默认为Null
        cur.next = ListNode(val)
        self.size += 1

    # 在链表的第 index 个节点前面插入一个节点
    def addAtIndex(self, index, val):
        # index 的合法性
        # 注意这里的 index > self.size 和其它地方不同,
        # 这里index=self.size是可以插到最后一位的,不算非法
        if index < 0 or index > self.size:
            return

        cur = self.dummy_head
        for i in range(index):
            cur = cur.next

        # 这样找到的cur就是下标为 index-1 的节点
        cur.next = ListNode(val=val,next=cur.next)
        self.size += 1

    # 删除链表的 第 index 个节点
    def deleteAtIndex(self,index):
        # index 的合法性
        if index < 0 or index >= self.size:
            return

        cur = self.dummy_head
        for i in range(index):
            cur = cur.next

        # 这样找到的cur就是下标为 index-1 的节点
        cur.next = cur.next.next
        self.size -= 1


  • 题目通过记录:

微信截图_20250114052507


4、206.反转链表


本节题目

对应力扣题目链接:https://leetcode.cn/problems/reverse-linked-list/description/


给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。


示例 1:


微信截图_20250114061049
  • 输入:head = [1,2,3,4,5]

  • 输出:[5,4,3,2,1]


示例 2 :

  • 输入:head = [1,2]

  • 输出:[2,1]


示例 3 :

  • 输入:head = []

  • 输出:[]



思路

如果再定义一个新的链表,实现链表元素的反转,那就简单太多了,并且这是对内存空间的浪费!

其实只需要改变链表的 next 指针的指向,直接将链表反转 ,而不用重新定义一个新的链表

如图所示 :


微信截图_20250114061449

那么看看动画演示是怎么反转的



  • 首先定义一个 cur 指针,指向头结点,再定义一个 pre 指针,初始化为 null

  • 首先要把 cur.next 节点用 tmp 指针保存一下 ,

  • 否则接下来如果改变 curnext 反转指向 pre 那么则会导致链表丢失

  • 然后反转指向 cur.next = pre

  • 然后先移动 pre 再移动 cur ,否则找不到 cur 现在指向的节点了

  • 也就是 先 pre = cur 然后 cur = tem

代码如下 :

python
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 初始化cur为head
        cur = head
        # 初始化pre为None
        pre = None

        while cur:
            # cur指到None才结束循环
            
            # 先保存cur的next,不然cur反转指针的时候会丢失链表
            tmp = cur.next
            
            cur.next = pre # 反转
            
            # 先移动pre 再移动cur
            pre = cur
            cur = tmp
            
        return pre


  • 题目通过记录

微信截图_20250114062759


Day 4


1、24.两两交换链表中的节点


本节题目

对应力扣题目链接:https://leetcode.cn/problems/swap-nodes-in-pairs/description/


给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。

你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。


微信截图_20250115015808


思路

这道题目正常 就行了 , 然后使用 会方便很多

接下来就是交换相邻两个元素了,

此时

初始时,cur 指向虚拟头结点,然后进行如下三步 :


微信截图_20250115161628


记得要在 保存 tmp1 , 否则

而且要在 保存 tmp2 ,否则

代码如下:

python
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 虚拟头节点
        dummy_head  = ListNode(next=head)

        # cur 初始化为虚拟头节点
        cur = dummy_head

        # 向后遍历
        while cur.next != None and cur.next.next != None:

            # 以第一步来注释说明
            tmp1 = cur.next             # 1.先保存首元结点和cur下一个位置的下一个节点
            tmp2 = cur.next.next.next   # 防止链表丢失
            cur.next = cur.next.next     # 2.cur指向下下个
            cur.next.next = tmp1        # 3. 第二个节点再指回首元结点
            tmp1.next = tmp2            # 之前的首元结点再指向第三个节点,完成第一步反转
            cur = tmp1                     # cur向后走
            
        return dummy_head.next



  • 题目通过记录:

微信截图_20250115162554


2、19. 删除链表的倒数第 N 个结点


本节题目

对应力扣题目链接:https://leetcode.cn/problems/remove-nth-node-from-end-of-list/description/


给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。


微信截图_20250115163230


快慢指针


双指针的经典应用,如果要删除倒数第 n 个节点,让 fast 移动n步,

然后让 fastslow 同时移动,直到 fast 指向链表末尾。删掉 slow 所指向的节点就可以了。

思路是这样,但要注意一些细节:

  • 首先推荐使用虚拟头结点 dummy_head 之前也说过了,在这种删除操作,虚拟头节点可以统一操作
  • 定义 fastslow 节点初始化为虚拟头节点

微信截图_20250115163819


  • fast 首先走 n + 1 步 ,

    因为只有这样同时移动的时候 slow 才能指向 (方便做删除操作)

    如图:


微信截图_20250115164030
  • fastslow 同时移动,直到 fast 指向末尾,如图 :

    ( 图片中有错别词:应该将 “只到” 改为 “直到” )


微信截图_20250115164258


  • 删除 slow 指向的下一个节点,如图:

微信截图_20250115164422


代码如下:

python
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        # 因为要走n+1步,先改变n
        n += 1

        # 虚拟头节点
        dummy_head = ListNode(next=head)

        # 初始化快慢指针都是虚拟头节点
        fast = slow = dummy_head

        # 快指针走 n+1 步 然后后续快慢指针一起走,这样距离就是 n+1
        # 最后slow就会在倒数第 n 个节点的前一个节点,就可以执行删除操作

        while n!= 0 and fast != None:

            fast = fast.next
            n -= 1

        # 出来的话,要不就是正常走了n+1,要不就是没走完就走到头了
        # 判断是哪种情况?? **略写**
        
        while fast != None:
            fast = fast.next
            slow = slow.next
            
        # 执行删除操作
        slow.next = slow.next.next
        
        return dummy_head.next



  • 题目通过记录:

微信截图_20250115164632


3、面试题 02.07. 链表相交


对应力扣题目链接:https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/description/


给你两个单链表的头节点 headAheadB

请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null


微信截图_20250115164826
微信截图_20250115164835
微信截图_20250115164843

微信截图_20250115164918


微信截图_20250115170146


思路

简单来说,就是求两个链表交点节点的 。但是要注意,交点不是数值相等,而是

看如下两个链表,目前 curA指向链表A的头结点,curB 指向链表B的头结点:


微信截图_20250115172540

我们求出两个链表的长度,并求出两个链表长度的差值,

然后让 curA 移动到,和 curB 末尾对齐的位置,

如图:


微信截图_20250115172628

此时我们就可以比较 curAcurB 是否相同,

如果不相同,同时向后移动 curAcurB

如果遇到 curA == curB,则找到交点。

否则循环退出返回空指针。

代码如下:

python
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        lenA, lenB = 0, 0
        # 遍历游标
        cur = headA
        # 求链表A的长度
        while cur:
            cur = cur.next
            lenA += 1

        cur = headB
        # 求链表B的长度
        while cur:
            cur = cur.next
            lenB += 1
            
        curA,curB = headA,headB
        if lenA > lenB:     # 让curB为最长链表的头,lenB为其长度
            curA,curB = curB,curA
            lenA,lenB = lenB,lenA
        
        for _ in range(lenB - lenA):
            # 让curA和curB在同一个起点上
            # 操作:curB移动(两个链表差值)个步数
            curB = curB.next
            
        while curA:
            # 遍历curA 和 curB , 遇到相同则直接返回
            
            if curA == curB:    # 这样的条件判断的是内存地址
                # 内存地址相同才算同一个节点
                return curA
            else:
                curA = curA.next
                curB = curB.next

        return None



  • 题目通过记录:

微信截图_20250115172819


4、142. 环形链表 II


本节题目

对应力扣题目链接:https://leetcode.cn/problems/linked-list-cycle-ii/description/


给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。

为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置

索引从 0 开始)。如果 pos-1,则在该链表中没有环。

注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。


微信截图_20250115173132
微信截图_20250115173139


思路

这道题目,不仅考察对链表的操作,而且还需要一些数学运算。

主要考察两知识点:

  • 判断链表是否环
  • 如果有环,如何找到这个环的入口

    1. 判断链表是否有环

可以使用快慢指针法,分别定义 fastslow 指针,从头结点出发,

fast 指针每次移动两个节点,slow 指针每次移动一个节点,

如果 fastslow 指针在途中相遇 ,说明这个链表有环。


❓ 为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢

首先第一点:

fast 指针一定先进入环中,如果 fast 指针和 slow 指针相遇的话,

一定是在环中相遇,这是毋庸置疑的。


那又正如刚刚所说,如果有环的话,就不能是在环中永远的恰好错开吗?

先明确一下,fast 的速度是一次两步,slow 的速度是一次一步,那么相对速度就是一次一步

在普遍的情况下, fast 就是这样一步一步的接近 slow , 然后最终都会演变成下图:


微信截图_20250116215747


上图那样,各自再走一步就相遇了 ...

动画如下:



    1. 如果有环,如何找到这个环的入口

此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。

假设 从头结点到环形入口节点 的节点数为 x 。 环形入口节点到 fast 指针与 slow 指针相遇节点

节点数为 y 。 从相遇节点 再到环形入口节点节点数为 z 。 如图所示:


微信截图_20250116220209


那么相遇时:

slow 指针走过的节点数为: x + y

fast 指针走过的节点数:x + y + n (y + z)

nfast 指针在环内走了 n 圈才遇到 slow 指针,

(y+z) 为 一圈内节点的个数 A


因为 fast 指针是一步走两个节点,slow 指针一步走一个节点,

所以 fast 指针走过的节点数 = slow 指针走过的节点数 × 2:

也就是 : (x + y) * 2 = x + y + n (y + z)

两边消掉一个 (x+y) : x + y = n (y + z)

因为要找环形的入口,那么要求的是 x ,因为 x 表示 头结点到 环形入口节点的的距离。

所以要求 x ,将 x 单独放在左面:x = n (y + z) - y ,

再从 n(y+z) 中提出一个 (y+z) 来,整理公式之后为如下公式:x = (n - 1) (y + z) + z

注意这里 n 一定是大于等于1的,因为 fast 指针至少要多走一圈才能相遇 slow 指针。


这个公式说明什么呢 ?

先拿 n 为 1 的情况来举例,意味着 fast 指针在环形里转了一圈之后,就遇到了 slow 指针了。

当 n 为 1 的时候,公式就化解为 x = z

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点,

那么当这两个指针相遇的时候就是 环形入口的节点

也就是在相遇节点处,定义一个指针 index1 ,在头结点处定一个指针 index2

index1index2 同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

动画如下:



那么 n 如果大于 1 是什么情况呢,就是 fast 指针在环形转 n 圈之后才遇到 slow 指针。

其实这种情况和 n 为 1 的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,

只不过,index1 指针在环里 多转了 (n-1) 圈,然后再遇到 index2 ,相遇点依然是环形的入口节点。

代码如下:

python
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # fast 和 slow 都初始化为 head
        fast = slow = head

        # 都遍历向后走,fast两步,slow一步
        while fast and fast.next:
            fast = fast.next.next
            slow = slow.next


            if fast == slow:
                # 两个指针相遇,移动一个指针回去head那
                slow = head
                while slow != fast:
                    slow = slow.next
                    fast = fast.next
                # 这里再相遇的位置就是环入口
                return slow
        return None

补充

在推理过程中,可能会有一个疑问就是:

为什么第一次在环中相遇,slow 的步数是 x+y ( 如下图所示位置 )

而不是 x + 若干环的长度 + y 呢?


微信截图_20250116224216

首先 slow 进环的时候,fast 一定是先进环来了。

如果 slow 进环入口,fast 也在环入口,那么把这个环展开成直线,

就是如下图的样子:


微信截图_20250116224423


可以看出如果 slowfast 同时在环入口开始走,一定会在环入口 3 相遇,

slow 走了一圈,fast 走了两圈。

重点来了,slow 进环的时候,fast 一定是在环的任意一个位置,

如图 :


微信截图_20250116224437


那么 fast 指针走到 环入口 3 的时候,已经走了 k + n 个节点,

slow 相应的应该走了 (k + n) / 2 个节点。

因为 k 是小于 n 的(图中可以看出),所以 (k + n) / 2 一定小于 n

也就是说 slow 一定没有走到环入口3,而 fast 已经到环入口3了

这说明什么呢?

在 slow 开始走的那一环已经和 fast 相遇了

那有同学又说了,为什么 fast 不能跳过去呢?

在之前已经说过一次了,fast 相对于 slow 是一次移动一个节点,所以不可能跳过去




  • 题目通过记录:

微信截图_20250116223909


Day 6


1、哈希表理论基础


哈希表

首先什么是哈希表,哈希表(英文名字为 Hash table,国内也有一些算法书籍翻译为 散列表

大家看到这两个名称知道都是指 hash table 就可以了)。

哈希表是根据 关键码 的值而直接进行访问的数据结构。

这么官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。

哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:


微信截图_20250117212435

那么哈希表能解决什么问题呢,

例如要查询一个名字是否在这所学校里。

要枚举的话时间复杂度是 O(n) ,但如果使用哈希表的话, 只需要 O(1) 就可以做到。

我们只需要初始化把这所学校里学生的名字都存在哈希表里,

在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。

将学生姓名映射到哈希表上就涉及到了



哈希函数

哈希函数,把学生的姓名直接映射为哈希表上的索引,

然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。

哈希函数如下图所示,通过 hashCode 把名字转化为数值,一般 hashcode 是通过特定编码方式,

可以将 ,这样就把学生名字映射为哈希表上的索引数字了。


微信截图_20250117212843

如果 hashCode 得到的数值大于 哈希表的大小了,也就是大于 tableSize 了,怎么办呢?

此时为了 上,我们会在再次对数值做一个 的操作,

这样我们就保证了学生姓名一定可以映射到哈希表上了。

此时问题又来了,哈希表我们刚刚说过,就是一个数组。

如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,

接下来 登场



哈希碰撞

如图所示,小李和小王都映射到了索引下标 1 的位置,


微信截图_20250117213328

一般哈希碰撞有两种解决方法,


  • 拉链法

刚刚小李和小王在索引 1 的位置发生了冲突,

这样我们就可以通过索引找到小李和小王了


微信截图_20250117213514

(数据规模是 dataSize , 哈希表的大小为 tableSize

其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,

也不会因为链表太长而在查找上浪费太多时间。


  • 线性探测法

使用线性探测法,一定要保证 tableSize 大于dataSize

我们需要依靠哈希表中的空位来解决碰撞问题 。

例如冲突的位置,放了小李,那么就

所以要求 tableSize 一定要大于 dataSize

要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:


微信截图_20250117213909


常见的三种哈希结构

当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。

  • 数组
  • set ( 集合 )
  • map ( 映射 )


2、242.有效的字母异位词


本节题目

对应力扣题目链接:https://leetcode.cn/problems/valid-anagram/description/


给定两个字符串 st ,编写一个函数来判断 t 是否是 s 的 字母异位词。

是通过重新排列不同单词或短语的字母而形成的单词或短语,并使用所有原字母一次。


微信截图_20250117214402


思路

先看暴力的解法,两层 for 循环,同时还要记录字符是否重复出现,很明显时间复杂度是 O(n^2)

暴力的方法这里就不做介绍了,直接看一下有没有更优的方式。

哈希表基础里提到,常见三种哈希结构有:数组,集合(set), 映射(map

那么 ,而且这道题目中字符串只有小写字符,

那么就可以定义一个数组,来记录字符串 s 里字符出现的次数。.

需要定义一个多大的数组呢,定一个数组叫做 record ,大小为 26 就可以了,

初始化为 0,因为

为了方便举例,判断一下字符串 s= "aee" , t = "eae"

操作动画如下:



定义一个数组叫做 record 用来上记录字符串 s 里字符出现的次数。

需要把字符映射到数组也就是哈希表的索引下标上,

因为字符 a 到字符 z 的 ASCII 是26个连续的数值,

所以我们需要使得字符 a 映射为下标 0 ,相应的字符 z 映射为下标 25。

关于字符和 ASCII 码值,在 python 里有两个函数可以互求

  • chr(i) :这是一个将 的字符的函数。
  • ord(c) : 这是一个将 ****的函数

那么这种情境下,我们就可以使用 ord() 求 ASCII 值

那么通过 : ord(某个小写字母) - ord(a) 这种计算方式

就可以把任意输入一个小写字母都可以映射到数组的 0-25 的范围内

我们就可以遍历 s 字符串,然后每个字符都经过上面的运算,

取到的值,数组对应下标 做 +1 操作 , 这样就可以实现统计了

同样在遍历字符串 t 的时候,对 t 中出现的字符映射哈希表索引上的数值再做 -1 的操作。

那么最后检查一下,record 数组如果

说明字符串 s 和 t 一定是谁多了字符或者谁少了字符,return false

最后如果 record 数组 ,说明字符串 s 和 t 是字母异位词,return true

时间复杂度为 O(n) ,空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为 O(1)

代码如下:

python
class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        # record 数组记录统计结果
        record = [0] * 26

        # 遍历字符串s 做+1操作
        for i in s:
            record[ord(i) - ord('a')] += 1

        # 遍历字符串 t 做-1操作
        for j in t:
            record[ord(j)-ord('a')] -= 1

        # 检查record数组
        for i in record:
            if i != 0:
                return False
        else:
            return True


  • 题目通过记录:

微信截图_20250118050141


补充

对于 python 来说,还有一些非常有用的内置模块可以作为这道题的其他解法


defaultdict

Python 中的 defaultdict 是一个非常好用的工具,它属于 collections 模块,是 dict 类的一个子类

❓ ​什么是 defaultdict

在普通的字典中,如果尝试访问一个不存在的键,会抛出 KeyError

defaultdict 可以自动为不存在的键提供一个默认值,

这个默认值的类型在创建 defaultdict 时就需要指定。

这样就避免了在使用字典时频繁地检查键是否存在,让代码更加简洁高效。


导入 collections 模块中的 defaultdict 类:

python
from collections import defaultdict

创建 defaultdict 对象 :

创建时

比如你想让 默认值是整数 0 ,就可以传入 int 函数,因为 int() 不带参数时会返回 0 :

python
dd = defaultdict(int)

此时 dd 就是一个默认值为 0 的 defaultdict 对象。

如果你想让 ,可以传入 list 函数:

python
dd = defaultdict(list)

使用 defaultdict 和普通字典很像,可以直接通过键来访问值,如果键不存在,就会自动使用默认值。

先看看 默认值为 0 的例子:

python
dd = defaultdict(int)
print(dd['a'])  # 输出0,因为 'a' 这个键不存在,自动使用默认值0
dd['b'] = 5
print(dd['b'])  # 输出5

再看 默认值为空列表 的例子:

python
dd = defaultdict(list)
dd['a'].append(1)
dd['a'].append(2)
print(dd['a'])  # 输出[1, 2]
print(dd['b'])  # 输出[],因为 'b' 这个键不存在,自动使用默认值空列表

本题代码如下:

python
class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        from collections import defaultdict
        
        s_dict = defaultdict(int)
        t_dict = defaultdict(int)
        for x in s:
            s_dict[x] += 1
        
        for x in t:
            t_dict[x] += 1
        return s_dict == t_dict

defaultdict 还有一个很好的应用,就是可以用在 的数据结构上:

下面我随机数随机构建一个图:

python
graph = defaultdict(set)

for i in range(10):     # 第几个节点
    for _ in range(3):  # 这个节点随机增加三个连接
        graph[i].add(random.randint(1,10))

print(graph)

运行结果:

txt
defaultdict(<class 'set'>, {0: {1, 6, 9}, 1: {3, 4, 7}, 2: {1, 10, 4}, 3: {8, 4, 5}, 4: {8, 2, 4}, 5: {8, 3, 7}, 6: {9, 5, 7}, 7: {2, 6, 7}, 8: {8, 10, 5}, 9: {9, 6, 1}})

这就生成了一幅 有向图

比如 0: {1, 6, 9} 即 0 号节点的出度为 3 ,分别指向 {1, 6, 9} ...



Counter

Python 中的 Counter 也是 collections 模块里的一个非常实用的类,它主要


❓ 什么是 Counter

Counter 是一个字典子类,专门用于统计元素出现的次数。

它可以对任意可哈希的对象进行计数,比如字符串、数字、元组等。

它会以元素为键,以元素出现的次数为值,创建一个计数器对象,方便我们快速统计和操作数据。


  1. 导入 collections 模块中的 Counter 类:
python
from collections import Counter
  1. 创建 Counter 对象

有多种方式可以创建 Counter 对象

  • 直接传入可迭代对象 :比如列表、字符串等
python
c = Counter(['red', 'blue', 'red', 'green', 'blue', 'blue'])
print(c)  
# 输出 Counter({'blue': 3, 'red': 2, 'green': 1})
python
c = Counter("gallahad")
print(c)  
# 输出 Counter({'a': 3, 'l': 2, 'g': 1, 'h': 1, 'd': 1})
  • 传入一个字典:字典的键是元素,值是元素出现的次数
python
c = Counter({'red': 4, 'blue': 2})
print(c)  
# 输出 Counter({'red': 4, 'blue': 2})
  • 使用关键字参数:参数名是元素,参数值是元素出现的次数
python
c = Counter(cats=4, dogs=8)
print(c)  
# 输出 Counter({'dogs': 8, 'cats': 4})
  1. Counter 对象的常用方法
  • 更新计数:使用 update() 方法可以对计数器进行更新
python
c = Counter(['red', 'blue'])
c.update(['blue', 'yellow'])
print(c)  
# 输出Counter({'blue': 2, 'red': 1, 'yellow': 1})

如果更新的对象是字典,会将字典中的键值对作为元素和出现次数进行更新。

  • 获取出现次数最多的元素:使用 most_common() 方法,

    可以指定返回出现次数最多的前 n 个元素及其出现次数

python
c = Counter(['red', 'blue', 'red', 'green', 'blue', 'blue'])
print(c.most_common(1))  # 输出[('blue', 3)]
print(c.most_common(2))  # 输出[('blue', 3), ('red', 2)]

如果不指定参数,则返回所有元素按出现次数从多到少排序的列表。

  • 计数器的运算:可以对两个 Counter 对象进行加、减等运算
python
c1 = Counter(['red', 'blue'])
c2 = Counter(['blue', 'yellow'])
print(c1 + c2)  # 输出Counter({'blue': 2, 'red': 1, 'yellow': 1})
print(c1 - c2)  # 输出Counter({'red': 1})

减法运算会自动去除结果中出现次数为 0 或 负数 的元素。


  • 实际应用场景

假设你有一个文本文件,想统计文件中每个单词出现的次数,就可以用 Counter 来轻松实现:

python
from collections import Counter

with open('example.txt', 'r') as file:
    words = file.read().split()

# word_counts = Counter(words)
word_counts = Counter()
for i in words:
    word_counts.update(Counter(i))
    
print(word_counts)

本题代码如下:

python
class Solution(object):
    def isAnagram(self, s: str, t: str) -> bool:
        from collections import Counter
        a_count = Counter(s)
        b_count = Counter(t)
        return a_count == b_count


3、349.两个数组的交集


本节题目

对应力扣题目链接:https://leetcode.cn/problems/intersection-of-two-arrays/description/


给定两个数组 nums1nums2 ,返回

输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序


微信截图_20250118234829




思路

这道题目,主要要学会使用一种哈希数据结构:unordered_set

这个数据结构可以解决很多类似的问题。

注意题目特意说明:输出结果中的每个元素一定是唯一的,

也就是说输出的结果的 的, 同时可以

这道题用暴力的解法时间复杂度是 O(n^2) ,那来看看使用哈希法进一步优化。

但是要注意,使用数组来做哈希的题目,是因为题目都限制了数值的大小

而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。

而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。

需要提及的是,在 C++ 中提供了如下三种可用的数据结构:

  • std::set
  • std::multiset
  • std::unordered_set

std::setstd::multiset 底层实现都是红黑树,std::unordered_set 的底层实现是哈希表,

使用 unordered_set 读写效率是最高的,并不需要对数据进行排序,

而且还不要让数据重复,所以选择 unordered_set

我们使用 python 的话,就可以使用提供的 set 这个数据结构,它也是可以去重

但不要忘记 力扣的 return 需要返回的是中括号的列表形式,最后记得转过来

思路如图所示:


微信截图_20250118235443

代码如下:

python
class Solution:
    def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        # 使用默认字典数据结构
        # 不需要判定字典有没有这个
        from collections import defaultdict

        result = set()	# set去重

        dd1 = defaultdict(int)

        for i in nums1:
            dd1[i] = 1		# 有这个数的标注为一就可以了

        for i in nums2:
            if i in dd1:
                result.add(i)
                del dd1[i]

        return list(result)


  • 题目通过记录:

微信截图_20250118234514


补充

那有同学可能问了,遇到哈希问题我直接都用 set 不就得了,用什么数组啊。

直接使用 set 不仅占用空间比数组大,而且速度要比数组慢,

set 把数值映射到 key 上都要做 hash 计算的。

不要小瞧 这个耗时,在数据量大的情况,差距是很明显的。


同时我们可以利用 python 的一些集合运算符很简洁的提交这道题

代码如下:

python
class Solution:
    def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        return list(set(nums1) & set(nums2))

仅仅一行代码就完成了这道题 !


顺便梳理一下有哪些集合的运算符:

  • 交集 (&): 返回两个集合的交集。
python
set1 = {1, 2, 3}
set2 = {2, 3, 4}
print(set1 & set2)  # 输出 {2, 3}
  • 并集 (|):返回两个集合的并集。
python
set1 = {1, 2, 3}
set2 = {2, 3, 4}
print(set1 | set2)  # 输出 {1, 2, 3, 4}
  • 差集 (-):返回一个集合中存在而另一个集合中不存在的元素。
python
set1 = {1, 2, 3}
set2 = {2, 3, 4}
print(set1 - set2)  # 输出 {1}
print(set2 - set1)  # 输出 {4}
  • 对称差集 (^): 返回两个集合中不同时存在的元素。
python
set1 = {1, 2, 3}
set2 = {2, 3, 4}
print(set1 ^ set2)  # 输出 {1, 4}


4、202.快乐数


本节题目

对应力扣题目链接:https://leetcode.cn/problems/happy-number/description/


编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n 是 快乐数 就返回 true ;不是,则返回 false


微信截图_20250119001033


思路

这道题目看上去貌似一道数学问题,其实并不是!

题目中说了会 无限循环,那么也就是说 求和的过程中,sum可能会重复出现,这对解题很重要!

正如前面 哈希表理论基础 所说 :

所以这道题目使用哈希法,来判断这个 sum 是否重复出现,

如果重复了就是 return false, 否则一直找到 sum 为 1 为止。

判断 sum 是否重复出现就可以使用 unordered_set ,也就是 python 的 原生 set

还有一个难点就是求和的过程,如果对取数值各个数位上的操作不熟悉的话,也会比较艰难

代码如下:

python
class Solution:
    def isHappy(self, n: int) -> bool:
        # 记录每次 sum 的结果,如果sum重复出现就可以 return false 了
        record = set()
        
        while True:
            n = self.get_sum(n)
            if n == 1:
                return True
            
            # 如果中间结果重复出现,说明进入死循环了,这个数不是快乐数
            if n in record:
                return False
            else:
                record.add(n)


    def get_sum(self,n):
        # 求 n 的各个数位的平方的和
        new_sum = 0
        while n:
            # n 为商,r为余数
            n, r = divmod(n, 10)
            new_sum += r ** 2

        return new_sum


  • 题目通过记录:

微信截图_20250119003011


5、1.两数之和


本节题目

对应力扣题目链接:https://leetcode.cn/problems/two-sum/description/


给定一个整数数组 nums 和一个整数目标值 target

请你在该数组中找出 和为目标值 target 的那 两个 整数,并 返回它们的数组下标。

你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。

你可以按任意顺序返回答案。


微信截图_20250119004501




思路

很明显暴力的解法是两层 for 循环查找,时间复杂度是 O(n^2)

再强调一下:

本题呢,就需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,

某元素是否遍历过,也就是 是否出现在这个集合。那么我们就应该想到使用哈希法了。

同时,本题不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,

需要使用 key:value 的结构,key 存元素,val 存下标,那么 map 正合适

也就是我们可以使用 pythondict 字典

存储结构为 {key:数据元素,value:数组元素对应的下标}

过程如下:


微信截图_20250119005907
微信截图_20250119005913

代码如下:

python
class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        # 有一个哈希表存放遍历过的元素
        record = dict()

        # 遍历 nums 数组
        for idx,val in enumerate(nums):

            # 需要在哈希表里查找的对象
            s = target - val

            if s in record:
                return [record[s],idx]
            
            # 记录下遍历过的 (遍历过的值:它的下标)
            record[val] = idx
        
        else:
            return []


  • 题目通过记录:

微信截图_20250119010325


Day 7


1、454.四数相加II


本节题目

对应力扣题目链接:https://leetcode.cn/problems/4sum-ii/description/


给你四个整数数组 nums1nums2nums3nums4 ,数组长度都是 n

请你计算有多少个元组 (i, j, k, l) 能满足:

  • 0 <= i, j, k, l < n
  • nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

微信截图_20250126161632


思路

本题这个题目一看感觉和 0015.三数之和0018.四数之和 差不多 , 其实差很多。

本题是使用 的经典题目 , 而 0015.三数之和0018.四数之和 并不合适使用 哈希法

因为 那两道题目使用哈希法在不超时的情况下做到对结果去重是很困难的,很有多细节需要处理。

而这道题目是 ,只要找到 A[i] + B[j] + C[k] + D[l] = 0 就可以 ,

不用考虑有重复的四个元素相加等于 0 的情况,所以相对于那两题,还是简单了不少 !

代码如下:

python
class Solution:
    def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
        
        from collections import defaultdict

        record = defaultdict(int)

        res = 0 # 需要返回的结果值

        # 遍历出 a + b 存进哈希结构map (字典dict)
        # 结构是 (a+b的和的值:a+b这个值出现的次数)
        for a in nums1:
            for b in nums2:
                # 默认字典的好处,第一次遍历到不用创建键,
                # 后面遍历到也可以直接加,这样可以统一操作了
                record[a+b] += 1

        # print(record)

        # 遍历出 c + d 然后 target = 0 - (c+d) ,在哈希表中找target
        # 找到一次加一次对应value
        for c in nums3:
            for d in nums4:
                target = 0-(c+d)
                if target in record:
                    # 找到加对应value,而不是加一
                    res += record[target]
                    
        return res


  • 题目通过记录:

微信截图_20250126162504


2、383.赎金信


本节题目

对应力扣题目链接:https://leetcode.cn/problems/ransom-note/description/


给你两个字符串:ransomNotemagazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true ;否则返回 false

magazine 中的每个字符只能在 ransomNote 中使用一次。


微信截图_20250126191507


思路

这道题和 242.有效的字母异位词 很像 , 242 相当于 求 字符串 a 和 字符串 b 是否可以相互组成,

而这道题目是求 字符串 a 能否组成 字符串 b , 而不用管 字符串 b 能不能组成 字符串 a

本题判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成,

但是这里需要注意两点 :

  • 第一点 “为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,

    组成单词来表达意思” 这里说明杂志里面的字母不可重复使用。

  • 第二点 “ 你可以假设两个字符串均只含有小写字母。” 说明只有小写字母,这一点很重要

代码如下:

python
class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        
        # 先遍历 magazine 里面出现的字符去哈希表里
        # 这个跟字符异位词不同的是,字符异位词是一一对应了,最后要求哈希表全为0
        # 而这个只需要magazine里面含有就可以了,也就是哈希表最后只要全大于等于0就对
        from collections import defaultdict

        record = defaultdict(int)

        for i in magazine:
            # magazine 加一操作
            record[i] += 1

        # 遍历ransomNote进行减一操作
        for j in ransomNote:
            record[j] -= 1

        for val in record.values():
            if val < 0:
                return False

        else:
            return True


  • 题目通过记录:

微信截图_20250126192116


3、15.三数之和


本节题目

对应力扣题目链接:https://leetcode.cn/problems/3sum/description/


给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]]

满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0

请你返回所有和为 0 且不重复的三元组。

注意: 答案中不可以包含重复的三元组。


微信截图_20250126192339


思路


  • 哈希法(不推荐)

两层for循环就可以确定 两个数值,可以使用哈希法来确定 第三个数 0-(a+b) 或者 0 - (a + c)

是否在 数组里出现过,其实这个思路是正确的,

但是我们有一个非常棘手的问题,就是题目中说的

把符合条件的三元组放进vector中,然后再 ,这样是非常费时的,

很容易超时,也是这道题目通过率如此之低的根源所在。

去重的过程不好处理,有很多小细节,如果在面试中很难想到位。

时间复杂度可以做到 O(n^2) ,但还是比较费时的,因为不好做剪枝操作。


  • 双指针法(推荐)

动画效果如下:



拿这个 nums 数组来举例,首先将 ,然后有一层 for 循环,i 从下标 0 的地方开始,

同时定一个下标 left 定义在 i+1 的位置上,定义下标 right 在数组结尾的位置上。

依然还是在数组中找到 a,b,c 使得 a + b +c =0

我们这里相当于 a = nums[i]b = nums[left]c = nums[right]

接下来

如果 nums[i] + nums[left] + nums[right] > 0 就说明

因为数组是排序后了,所以 right 下标就应该向左移动,这样才能让三数之和小一些。

同理如果 nums[i] + nums[left] + nums[right] < 0 说明

left 就向右移动,才能让三数之和大一些,直到 leftright 相遇为止。

代码如下:

python
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        
        # 收集结果集的列表
        result = []

        # 对传入的nums进行排序
        nums.sort()

        # 遍历 i
        for i in range(len(nums)):
            
            # 如果一个数就大于0就没必要往后走了,剪枝操作
            if nums[i] > 0:
                break
            
            if i > 0 and nums[i] == nums[i - 1]:
                # i 的去重在这里完成
                continue

            # 初始化 left 和 right
            left = i + 1  # left 是 i 右边一位
            right = len(nums) - 1  # right 是 nums 的最后一位开始

            while (right > left):  # 不能相等,相等就不是三元组了
                target = 0 - nums[i]
                if nums[left] + nums[right] == target:
                    # 这就是一组结果集
                    result.append([nums[i], nums[left], nums[right]])

                    # left 和 right 的去重
                    # 注意要在收集至少一个结果后进行
                    while left < right and nums[left + 1] == nums[left]:
                        left += 1

                    while right > left and nums[right - 1] == nums[right]:
                        right -= 1

                    # 确保 left 和 right 的下一个位置都不会导致重复结果后
                    # 进行同时向下一步遍历
                    left += 1
                    right -= 1

                elif nums[left] + nums[right] > target:
                    # 大了的话,right向下走
                    right -= 1

                else:    # nums[left] + nums[right] < target
                    # 小了的话,left 往后走
                    left += 1

        return result


  • 题目通过记录:

微信截图_20250126193838


去重的思考

说到去重,其实主要考虑 三个数的去重。

a, b ,c , 对应的就是 nums[i]nums[left]nums[right]

  • a 的去重

a 如果重复了怎么办,a是 nums 里遍历的元素,那么应该直接跳过去。

但这里有一个问题,是判断 nums[i]nums[i + 1] 是否相同,

还是判断 nums[i]nums[i-1] 是否相同。

有同学可能想,这不都一样吗。 其实不一样!

都是和 nums[i] 进行比较,是比较它的前一个,还是比较它的后一个。

如果我们的写法是 这样:

python
if nums[i] == nums[i + 1]:  # 去重操作
    continue

那我们就把 三元组中出现重复元素的情况直接 pass 掉了。 例如 {-1, -1 ,2} 这组数据,

当遍历到第一个 -1 的时候,判断 下一个也是 -1,那这组数据就 pass了。

我们要做的是

所以这里是有两个重复的维度。

那么应该这么写:

python
if nums[i] == nums[i - 1]:  # 去重操作
    continue

这么写就是当前使用 nums[i],我们判断前一位是不是一样的元素,

再看 {-1, -1 ,2} 这组数据,当遍历到 第一个 -1 的时候,只要前一位没有-1,

那么 {-1, -1 ,2} 这组数据一样可以收录到 结果集里。

这是一个非常细节的思考过程。

  • b 和 c 的去重

在收集到一组结果集后,我们可以对 b,c 进行去重

因为本组遍历的 i 是不变的,如果收集到一组满足条件的结果的话

任意变 b,c 其中一个,都会导致和前面那组三元组是完全一样的

python
if nums[left] + nums[right] == target:
    # 这就是一组结果集
    result.append([nums[i], nums[left], nums[right]])

    # left 和 right 的去重
    # 注意要在收集至少一个结果后进行
    while left  < right and nums[left + 1] == nums[left]:
		left += 1

	while right > left and nums[right - 1] == nums[right]:
		right -= 1


4、18.四数之和


本节题目

对应力扣题目链接:https://leetcode.cn/problems/4sum/description/


给你一个由 n 个整数组成的数组 nums ,和一个目标值 target

请你找出并返回满足下述全部条件且

[nums[a], nums[b], nums[c], nums[d]]

(若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abcd 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

你可以按 返回答案 。


微信截图_20250126201027


思路

四数之和,和 15.三数之和 是一个思路 , 都是使用

基本解法就是在 15.三数之和 的基础上再套一层 for 循环

但是有一些细节需要注意,

例如:不要判断 num[k] > target 就返回了,三数之和 可以通过 num[i] > 0 就返回了,

是因为 0 已经是确定是数了, 四数之和这道题目的 target 是任意值。

比如 :数组是 [-4, -3, -2, -1]target-10

不能因为 -4 > -10 而跳过。但是我们依旧可以去做剪枝,

逻辑变成 nums[i] > target and (nums[i] >=0 or target >= 0) 就可以了。


  • 解法对比:

15.三数之和 的双指针解法是一层 for 循环 num[i] 为确定值,

然后循环内有 leftright 下标作为双指针,找到 num[i] + num[left] + num[right]== target

18.四数之和 的双指针解法是两层 for 循环 nums[k]+nums[i] 为确定值,

依然是循环内有 leftright 下标作为双指针,

找出 nums[k] + nums[i] + nums[left] + nums[right] == target 的情况,

三数之和的时间复杂度是 O(n^2) , 四数之和的时间复杂度是 O(n^3)


那么一样道理,五数之和、六数之和等等都采用这种解法。

对于 15.三数之和 双指针法就是将原本暴力 O(n^3),降为 O(n^2) 的解法,

四数之和的双指针解法就是将原本暴力 O(n^4) 的解法,降为 O(n^3) 的解法。

代码如下:

python
class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        # 收集结果集的列表
        result = []

        # 对传入的nums进行排序
        nums.sort()

        for k in range(len(nums)):

            # 一级剪枝
            if (nums[k] > 0 or target > 0) and nums[k] > target:
                break

            # k 去重
            if k > 0 and nums[k] == nums[k-1]:
                continue

            for i in range(k+1,len(nums)):

                # 二级剪枝
                if (nums[k] + nums[i] > 0 or target > 0) and nums[k] + nums[i] > target:
                    break

                # i 去重
                if i > k+1 and nums[i] == nums[i-1]:
                    continue

                # 下面就是三数之和中的双指针的逻辑
                
                # 初始化left 和 right 
                left = i + 1
                right = len(nums) - 1
                
                while left < right:
                
                    if nums[k] + nums[i] + nums[left] + nums[right] == target:
                        # 符合条件就加入结果集
                        result.append([nums[k],nums[i],nums[left],nums[right]])
                        
                        # 加入结果集后要做双指针的去重
                        while left + 1 < right and nums[left] == nums[left+1]:
                            left += 1
                            
                        while right - 1 > left and nums[right] == nums[right-1]:
                            right -= 1
                            
                        # 去重后同时向内走一步
                        left += 1
                        right -= 1
                        
                    elif nums[k] + nums[i] + nums[left] + nums[right] > target:
                        # 大了的话 ,right就向内收
                        right -= 1
                        
                    else: # nums[k] + nums[i] + nums[left] + nums[right] < target
                        # 小了的话,left向内收
                        left += 1
                    
        return result


  • 题目通过记录:

微信截图_20250127152014


Day 8


1、344.反转字符串


本节题目

对应力扣题目链接:https://leetcode.cn/problems/reverse-string/description/


编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。

不要给另外的数组分配额外的空间,

你必须 原地修改输入数组 、使用 O(1) 的空间复杂度解决这一问题。