拜伦的博客

Coder Byron

这里有关于iOS,机器学习的笔记心得,欢迎交流


LeetCode刷题笔记

目录

链表和数组

多见链表的全反转,K个一组反转链表 head 等表示指定Node,不是index也不是指针 head.next = node1 表示该Node的后继节点是node1 node2 = node1.next, 表示将node1的后继节点node赋值给 node2

0. 交换链表节点

剑指 Offer 反转链表

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

  def reverseList(self, head: ListNode) -> ListNode:
        cur = head
        prev = None
        while cur:
            cur.next, prev, cur = prev, cur, cur.next
        return prev

两两交换链表中的节点

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。 给定 1->2->3->4, 你应该返回 2->1->4->3.

 def swapPairs(self, head):
        a, a.next = self, head
        while a.next and a.next.next:
            b,c = a.next, a.next.next
            a.next, b.next, c.next = c, c.next, b
            a = b
        return self.next

1. 判断链表是否有环

判断是否有环可用快慢指针, 起点相同,快指针每次走2个步,满指针每次都1步,如果最终快慢指针相遇,则证明此链表中有环。 用set将路径上每个节点存起来,每走一步判断set中是否有当前节点,若有则证明有环

环形链表

给定一个链表,判断链表中是否有环

def hasCycle(self, head: ListNode) -> bool:
    # 快慢指针法
        fast = slow = head
        while fast and slow and fast.next:
            fast = fast.next.next
            slow = slow.next
            if (fast == slow):
                return True
        return False

环形链表 II

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

def detectCycle(self, head: ListNode) -> ListNode:
    # 快慢指针不好使,快指针可能会跳过那个节点
    # 所以还是使用的缓存法,把走过的路缓存起来
        cur = head
        cache = set()
        while cur:
            if cur not in cache:
                cache.add(cur)
                cur = cur.next
            else:
                return cur
        return None

K 个一组翻转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。 k 是一个正整数,它的值小于或等于链表的长度。 如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

2. 判断括号字符串是否有效

用堆栈,如果符号是左边,入栈,如果符号是右边则与栈顶符号做匹配。最终堆栈为空则有效 将字符串中所有{} [] () 替换为空字符串,如果最后又剩余则表示字符串无效

有效的括号

给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串,判断字符串是否有效。

def isValid(self, s: str) -> bool:
         #堆栈的方式
        symbols = {'}':'{',']':'[',')':'('}
        stack = []
        for i in s:
            if i not in symbols:
                stack.append(i)
                # if stack为空意思是 如果栈还是空的就来一个右符号,则肯定是无效的
                # 或者是栈取出来的不一样
            elif not stack or symbols[i] != stack.pop():
                return False
        return not stack

3. 用队列实现栈&用栈实现队列

队列-> 栈: 每个元素入栈后,将前面n-1个元素pop然后压入栈,保证第n个元素总是位于第一位的。 栈->队列: 用2个栈, A为输入栈,B为输出栈。 当pop时将A所有元素依次出栈并压入B栈,形成倒叙的A栈数据。后续若B栈为空,则继续讲A倒叙压入B栈中再执行pop

用队列实现栈

    def __init__(self):
        self.q = []

    def push(self, x: int) -> None:
        self.q.append(x)

    def pop(self) -> int:
        temp = self.q[-1]
        del self.q[-1]
        return temp

    def top(self) -> int:
        return self.q[-1]

    def empty(self) -> bool:
        return not len(self.q)

用栈实现队列

    def __init__(self):
        self.s1 = []
        self.s2 = []

    def push(self, x: int) -> None:
        self.s1.append(x)

    def pop(self) -> int:
        if not len(self.s2):
            for _ in range(len(self.s1)):
                self.s2.append(self.s1.pop())
        return self.s2.pop()

    def peek(self) -> int:
        if not len(self.s2):
            for _ in range(len(self.s1)):
                self.s2.append(self.s1.pop())
        return self.s2[len(self.s2)-1]

    def empty(self) -> bool:
        return len(self.s1) == 0 and len(self.s2) == 0

4. 返回数据流中的第K大元素

用最小堆,维护一个大小为K的最小堆,每次比较当前元素与堆顶元素,若当前元素大,则删除堆顶元素,加入当前元素并重新排序,然后继续扫描下一个元素

5. 返回滑动窗口中的最大值

使用最大堆,同上理 使用双端队列, 每次窗口扫到一个新元素,则将新元素与窗口中第一个元素比较大小,如果新元素大,则将窗口中前面所有元素删除,新元素成为第一个元素。如果旧窗口中的元素大,则不作处理,新元素加入窗口中。 此举目的是保证窗口中始终把最大元素放在最左边位置即第一位,后续扫描只需要比较一次大小即可。

6. 有效的字母异位词

单词中每个字母个数相同,排序不同。 使用排序,排序后比较是否一致 使用hashMap, 将每个字母出现的次数统计在map中,最后比较map是否一致

有效的字母异位词

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

# 排序的方式
def isAnagram(self, s: str, t: str) -> bool:
        return sorted(s) == sorted(t)
# hashMap 的方式
    def isAnagram(self, s: str, t: str) -> bool:
        voc1 = {}
        voc2 = {}
        for char in s:
            voc1[char] = voc1.get(char, 0) + 1
        for char in t:
            voc2[char] = voc2.get(char, 0) + 1
        return voc1 == voc2

7. 两数之和

2个for循环,暴力计算 用set, 遍历第一层,计算set中是否包含target-item的值

两数之和

def twoSum(self, nums: List[int], target: int) -> List[int]:
        res = {}
        # 遍历nums, 输出index和每个item值, 这里map中存的是数值的index,方便返回
        for i, item in enumerate(nums):
            if (target-item) not in res:
                res[item] = i
            else:
                return [res[target-item], i]
        return []

8. 三数之和,输出所有三个数字之和为0的组合

3层for循环,暴力计算,同上 用set,2个for循环,计算set中是否包含-item1-item2的值

三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

def threeSum(self, nums):
    nums.sort()
    res = set()
    for i,item in enumerate(nums[:-2]):
        # 此判断为了能快速判断,若item与前一位值相同则继续循环
        # 因为如果前面相同值有解,后面item的解一定与前面的相同,要求是答案中是不重复的,所以可以排除不遍历
​        if i >= 1 and item == nums[i-1]:
​            continue
​        d = {}
​        for item2 in nums[i+1:]:
​            if item2 not in d:

                # 这里的处理方式同两数之和,前2个数的负数就是第3个解,3数值和为0.
​                d[-item-item2] = 1
​            else:
​                res.add((item, -item-item2, item2))
​    return map(list, res)

延伸 输出三数之和为0的所有index组合

9. 验证二叉排序树

中序遍历整个树,判断是否为有序数组 递归,每次递归当前节点的左子树,然后判断根节点的值是否比前继节点值大

验证二叉搜索树

给定一个二叉树,判断其是否是一个有效的二叉搜索树即二叉平衡树

def isValidBST(self, root: TreeNode) -> bool:        
        self.prve = None
        def isBST(root):
            if not root:
                return True
            if not isBST(root.left):
                return False 
            # 这里为什么要小于 前继节点的值?
            # 因为这是中序遍历,会把左子树全部走完才会走到每个树的根节点,所以prve一定是比根节点小的值,因为都是左子树嘛
            # 那么这里一定是更节点 > prve的值
            if self.prve and self.prve.val >= root.val:
                return False
            # 这里赋值的原因是,要拿根节点与右子树比较
            self.prve = root 
            return isBST(root.right)
        return isBST(root)

10. 二叉树&二叉搜索树的最近公共祖先

从链表角度考虑,由子节点往根节点遍历,路径上所有点记录下来,比较2个路径中最后的相同节点。 递归,分别寻找左子树和右子树中是否包含P 和 Q这2个节点,如果包含返回节点,不包含则继续递归直到子树中出现节点或者到达叶子节点返回空,那么这个树就将单一的P或Q一直往上透传,直到在某个节点中与取到的P或Q组成一对,则返回当前根节点的值即为公共祖先

二叉搜索树的最近公共祖先

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

 def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        # 简单递归
        def lower(root, p, q):
            if not root or root == p or root == q : return root
            left = lower(root, p,q) # 结果是p 或 q 或叶子节点
            right = lower(root, p ,q)
            if not left: return right # 如果左子树没有结果,结果一定在右边
            if not right: return left # 同理
            if left and right : return root # 如果左右子树都有值,说明当前节点就是左右子树的共同根节点
        return lower(root, left, right)
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        # 排序递归
        if  root.val< p and root.val< q:
            return lowestCommonAncestor(root.right, p,q)
        if  root.val> p and root.val> q:
            return lowestCommonAncestor(root.left, p,q)
        if  root.val>p and root.val < q:
            return root

二叉树的最近公共祖先

# 同上题简单递归,因为此题无序,无法使用排序逻辑
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        def lowest(root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
            # 此处如果root是空,也可以返回,表示后续没找到,走8,9行判断
            if not root or root == p or root == q: return root
            left = lowest(root.left, p, q)
            right = lowest(root.right, p, q)
            if not left: return right
            if not right:return left
            if left and right: return root
        return lowest(root, p, q)

11. Pow(x,n) 即x的n次方

递归,分治的思想,x的n次方可以拆分为 x的n/2次方*x的n/2次方, 还可以继续往下拆分,有效避免了重复计算,时间复杂度是logn.

Pow(x, n)

实现 pow(x, n) ,即计算 x 的 n 次幂函数。

 # 使用递归方式,分治
    def myPow(self, x: float, n: int) -> float:
        if not n : return 1
        if n < 0 : return (1 / self.myPow(x, -n))
        if n %2 : # 奇数
            return x * self.myPow(x, n-1)
        return self.myPow(x*x, n/2) # 偶数

12. 求众数,即元素出现次数大于n/2

暴力计算,2个loop, O(n²) HashMap, 遍历所有元素,把出现次数保存到map中,再查找map中多次数的元素 排序后获取数组中间位置即为众数 分治思想,获取左右子集中的众数,如果左右众数一致则得出结果,不一致则继续分治拆分左右子集,统计出所有子集的众数,最多的为众数

多数元素

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。

def majorityElement(self, nums: List[int]) -> int:

    # 摩尔投票法, 当一个数的重复次数超过数组长度的一半,每次将两个不相同的数删除,最终剩下的就是要找的数。
        res = 0
        count = 0
        for item in nums:
            if count == 0:
                res = item
                count = 1
            elif item == res:
                count += 1
            else:
                count -= 1
        if count > 0: return res

def majorityElement(self, nums: List[int]) -> int:
    # 哈希法
        res = {}
        maxItem = 0
        maxNums = 0
        for item in nums:
            res[item] = res.get(item, 0) + 1
        for item, value in res.items():
            if maxNums < value : 
                maxItem = item
                maxNums = value
        return maxItem

13. 买卖股票的最佳时机

DFS 深度优先搜索 贪心算法: 选择局部最优 动态规划

14. 二叉树层次遍历

BFS, Breadth First Search 广度优先搜索 DFS, Depth First Search 深度优先搜索

二叉树的层序遍历

def levelOrder(self, root: TreeNode) -> List[List[int]]:   
        # BFS 用队列
        queue = []
        queue.append(root)
        result = []
        while queue:
            size = len(queue)
            current_level = []
            for _ in range(size):
                node = queue.pop(0)
                current_level.append(node.val)
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            result.append(current_level)
        return result
     

def levelOrder(self, root: TreeNode) -> List[List[int]]:   
        # DFS用栈
        result = []
        def dfs(node, level):
            if not node: return []
            if len(result) < level+1:
                result.append([])
            result[level].append(node.val)
            dfs(node.left, level+1)
            dfs(node.right, level+1)
        dfs(root,0)
        return result

15. 二叉树的最大和最小深度

同上,可以使用BFS 和 DFS方法解决。 BFS则是使用 while循环遍历当前层级的queue中所有对象,(最小)如果其中一个节点是叶子节点,则结束遍历,输出当前层级即为最小深度。(最大)一直遍历整棵树,输出最后的层级即为最大深度 DFS则使用递归,递归每层的左右节点,结束条件是若当前节点是空返回0.

16. 生成有效括号组合

同上,使用DFS递归来解决,注意2个递归条件,已使用的右括号必须小于左括号,才可继续增加右括号,否则没有多于的左括号去配对。当左右括号全部使用完毕将字符串添加到结果list中

17. N皇后问题

使用DFS递归, 思想是类似围棋或象棋算法每一步都会产生一个分支,每个分支下一步棋又会产生一个分支,每条分支的每一步都会有成功或者失败的判断即剪枝的条件,如剪枝成立则此分支结束,回溯到上一分支继续走其他步。上上一步也可回溯继续走下去。只要某个条件下能够走将N个皇后走完,则代表前面的每一步都是成立的,且路径都会有记录。 使用DFS+位运算, 太复杂, 后续再研究

18. 数独问题

依然使用DFS递归然后回溯去搜索解,剪枝的条件是横竖现有值不能重复,33的9宫格中数字不能重复。除此判断条件外,直接递归回溯直到走完所有99输出答案

19. 实现一个求解平方根的函数

用二分查找法,因为y=x²是单调递增,所以是有序有界的,可以通过mid*mid来逼近真实的x 用牛顿迭代法,有数学公式

20. 实现一个字典树即Trie树 字典树

字典里面套字典,表示每个节点有多个边相对应,穿起来所有路径即为最终的单词,Trie树如下图, 例如insert “apple” ,则生成Trie树为 {‘a’: {‘p’: {‘p’: {‘l’: {‘e’: {‘#’: ‘#’}}}}}} 最后一个字母e的分支只有结束标识符”#” 在上面的基础上insert “app”, 则树为 {‘a’: {‘p’: {‘p’: {‘l’: {‘e’: {‘#’: ‘#’}}, ‘#’: ‘#’}}}} 在p的分支有2个,一个是”l”, 一个是结束符”#” ,代表app可以结束,也可以只是前缀

21. 二维网格中的单词搜索问题

在二维网格中搜索单词,可以用DFS,去递归+回溯。 也可用DFS+Trie树+回溯。把单词编制成Trie树,遍历网格中每个单词,若是树中的首字母,则接着递归首字母附件的4个字母是否存在于树中,不存在则回溯

22. 统计位1的个数

先学习一下常见的位运算 x&1 == 1 or == 0 判断奇偶性 x = x & (x-1) 清除x最低位的1 x&-x 拿到最低位的1

23. 2的幂次方问题&比特位计数问题

2的幂次方有个特点,2进制只有1位是1,其他全是0,可以用上面的位运算解决 比特位计数问题如果用上面的暴力算法是可以解决的,问题是每个数字都要计算一遍浪费时间,最好的方式是前面计算的结果,后面直接使用,公式是count[x] = count[x&(x-1)] +1 ,就是 x比x小1位的数字的个数+1. 这个一个循环得到结果,很巧妙

2的幂

# 2的幂次方 只有一位是1,其他全是0, 所以打掉1之后 就是0了
def isPowerOfTwo(self, n: int) -> bool:
        if n != 0 and n&(n-1) == 0:
            return True
        else:
            return False

24. 动态规划

  1. 递推 (递归+记忆化)
  2. 状态的定义: opt[n], dp[n]…
  3. 状态转移方程: opt[n] = best_of(opt[n-1] ,opt[n-2] …)
  4. 最优子结构: 最终的结果是由最优的每一步得到的,每一步的最优结果都得到保存,所以如果问子结构中最优结果就可以直接得到 例如斐波拉切数列,F[n] = F[n-1] + F[n-2], 直接递归会有很多重复计算,例如F[4]=F[3]+F[2] = F[2]+F[1]+F[2],重复计算了F[2],如果将F[2]结果保存起来,就节省很多时间。

25. 动态规划DP vs 回溯 vs 贪心

回溯(递归) -> 重复计算, 可以做到总体最优,但是计算量大,性能低下 贪心 -> 永远局部最优,如果简单的每步局部最优就是总体最优,这样是可以的,但是往往没这么简单 DP -> 记录局部最优子结构, 其实如果能够递归的问题,一般都可通过DP解决

26. 爬楼梯

递推公式同 斐波拉切 F[n] = F[n-1] + F[n-2] 因为最后一步有多少种方法等于 前一步的方法 加上前2步的方法, 因为可以一次可以1个台阶或者2个台阶 DP定义:到第n层有多少种方法

27. 三角形的最小路径和

一种递归+回溯,能解决,只是时间很长,重复计算 动态规划,从底层开始,往上遍历,上层每次都选择下面2个其中小的一个, 推导出的DP转移方程为F[j]= min(F[j], F[j+1]) + t[i][j] F[j]为指定层第j个位置到达底部最小路径,以此类推,顶部只需要计算下一层到他的最小路径即可。 DP定义:到第j层最小路径

28. 乘积最大子数组

动态规划,计算连续的子数组积的最大值, 由于元素可能有正,有负, 如果始终只保留正数,可能后续元素出现负数积就变小了,可能之前的负数与负数的积是最大值。所以需要保留之前的最大值和最小值,乘后续数组元素后再覆盖最大值最小值 DP定义:到第i位最大值和最小值

29. 买卖股票的最佳时机 & 买卖股票的最佳时机 III & 买卖股票的最佳时机 II

用3维数组,分别代表0~n-1天, 0/1是否持股, 0~k次交易 能够满足大部分股票买卖的状态定义

30. 最长上升子序列

dp[i]状态为 在i点包含i的最长上升子序列, 不是i点的最长上升子序列,所以最后的结果是选择数组中最大的,而不是指定位置的

31. 零钱兑换

dp[i] 为凑成当前金额最少的硬币数,则dp方程为 dp[i] = min{dp[i-conins[j] ]} +1, 例如银币金额是[1,2,5], 则dp[11] = min{ dp[10], dp[9], dp[6]} +1 可以把此题当成爬楼梯,到达11层,一次可以跨1步,2步,5步,最少需要多少步一样。

32. 编辑距离

把A单词转换成B单词最少需要多少步, 可以插入,删除,替换。dp[i][j] 表示从A单词的 i 位置到B单词 j 位置需要的最小操作数。 dp方程为dp[i][j] = if A[i] ==B[j] : dp[i-1][j-1] else: min{dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}+1 如果当前ij字符相等,则dp[i][j]就等于上一步的结果,不需要变动。如果不等,则等于插入,删除,替换其中一个的最小步数+1

33. DP套路 三部曲

  1. 套路步骤1, 创建dp状态定义
  2. 套路步骤2, 初始化dp首位状态
  3. 套路步骤3, 大循环开始写递推公式,并赋值给dp状态

34. 并查集

几个重要方法, 初始化,查找父类,合并。 查找父类可以做路径压缩的优化。

35. 岛屿数量

染色法:遍历节点,把节点为“1”的相邻节点赋值为“0”, 并记录为一个岛。可用DFS也可BFS。 并查集:对为“1”的节点初始化,把相邻的合并,最后遍历有多少个roots

36. 布隆过滤器

是一个很长的二进制向量和一个映射函数,用于检索一个元素是否在集合中,如果判断不在集合中,那一定不在集合中。如果判断在集合中,不一定再 特点: 空间效率和查询时间远远超过一般算法,但是有一定误识别率和删除困难。

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦