一、链表
1、链表基础理论
1、单链表、双链表、循环链表
2、经常用到的四种指针、虚拟节点
#前指针
pre
# 当前指针
cur
# 后指针
post
#临时指针
temp
# 虚拟头节点
dummy_head # 涉及有可能对头节点进行的操作时,千万设立一个虚拟头节点
# 使用方法
# 先定义节点
dummy_head = ListNode(0) #
# 再指向链表
dummy_head.next = head
2、创建节点、遍历节点、改变节点、删除节点、插入节点
# 创建类
class ListNode(object):
self __init__(self, val, next = None):
self.val = val
self.next = None
# 创建一个值为0的节点
cur = ListNode(0) #
# 指针下移
cur =cur.next
cur.next = post # 改变节点
# 遍历
while cur!= None:
cur= cur.next
# 删除节点(可能需要两个指针) 想要删除cur
pre.next = cur.next
或者
pre.next = pre.next.next
# 插入节点(在cur之后插入 node1)
node1.next = cur.next
cur.next = node1
链表题目
:删除链表的节点(指定节点的值)
前后指针法
:反转链表
# 前后指针+临时指针 (三指针:pre用来遍历原始链表,temp保存pre的下一个节点,cur用来组建反转后的链表)
# 注意起始要有一个none节点(初始cur为none,然后不断将pre遍历到的插入cur之前)
:两两交换链表中的节点
:链表中倒数第 k 个节点
快慢指针法 :快指针先走k步,慢指针再走
:两个链表的第一个公共节点
# 从两个头指针处分别出发,走到头后返回另一个头指针,第一次相等处即为第一个公共节点
# 如果没有公共节点,则第一次会在 null相遇,此时返回,同样满足题意
# 理解:把长度分成3段理解,交点之后公共部分走的一样长设为c,交点之前的长度分别设为a和b
# 那么 a=b,时,直接相遇;不等时, 第二次走到交点处 :a+c+b = b + c + a
:环形链表
# 判断是否有环用快慢指针
# 快慢指针法
# 起点相同
fast = slow = head
# fast一次前进两步,所以要判断更新后的当前位置和下一位置是否为空
# 对快指针而言,循环终止条件是当前指针为空或者下一指针为空
# 即 满足循环的条件为:当前节点和下一节点都不为空
# 对于fast.next.next,其实就是下一次循环的fast,固不需在本次循环中判断
# != None 可不写
while fast and fast.next:
fast = fast.next.next # 每次前进两步
slow = slow.next # 每次前进一步
if fast == slow: # 比较两个指针指向的地址
return True
return False
# 环的入口
# 快慢指针
# 第一次相遇,其中一个返回头节点,再同速,再次相遇点为起点
fast = slow = head
# fast一次前进两步,所以要判断更新后的当前位置和下一位置是否为空
# != None 可不写
while fast != None and fast.next != None:
fast = fast.next.next
slow = slow.next
if fast == slow:
fast = head
while fast != slow:
fast = fast.next
slow = slow.next
return fast
return None
:合并两个排序的链表
用cur保存小的那个节点,设立伪头节点
# 伪头节点 dum + 先比较(小于等于的先合并至cur)+ 被合并的向下遍历一个节点继续遍历 + 最后剩余的部分直接合并
dum = ListNode(0) # 最后返回dum.next
cur = dum # cur 为合并后的链表的当前节点
# 先比较合并
while l1 != None and l2 != None:
if l1.val <= l2.val:
cur.next = l1
l1 = l1.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next # cur也要向下移动
# 再合并剩余
if l1 != None:
cur.next = l1
else:
cur.next = l2
return dum.next
:设计链表(插入、删除等各种操作等,与第二点相似)
:删除重复元素链表
1)链表升序,且保留1个重复的
遍历链表,和下一个节点值相同则删除(删除后,cur不动,和下一个继续比),不相同则下移,重复操作,直到none
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution(object):
def deleteDuplicates(self, head):
"""
:type head: ListNode
:rtype: ListNode
"""
# 遍历链表,和下一个节点值相同则删除(删除后,cur不动,和下一个继续比),不相同则下移,重复操作,直到none
# 考虑为空的情况
if head == None:
return head
cur = head
while cur and cur.next:
if cur.val == cur.next.val:
cur.next = cur.next.next
else:
cur = cur.next
return head
2)升序,且不保留重复的
基础操作:#向下遍历#cur = cur.next #删除链表元素# cur.next = cur.next.next
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution(object):
def deleteDuplicates(self, head):
"""
:type head: ListNode
:rtype: ListNode
"""
# dummy指向头节点(避免head被删除的情况)
dummy = ListNode(0, head) # 要传递两个参数,不能只给一个,val的值任意给
cur = dummy
while cur.next and cur.next.next:
if cur.next.val == cur.next.next.val: # cur相当于前指针,把后一个和后两个拿来比,不断删除重复的(重复值可以单独用一个数存储,方便比较);重复值的下一个节点不同时,cur不用动;只有后一个和后两个直接比较不同时,cur才移动
# 保存重复的元素值
x = cur.next.val
# 不断删除重复的
while cur.next and cur.next.val == x:
cur.next = cur.next.next
else:
# 向下遍历一位
cur = cur.next
# 返回dummy的下一个
return dummy.next
二、动态规划
1、题目
:剑指 Offer 47. 礼物的最大价值
# 针对在第一行、第一列、其他情况下的dp[i][j]进行分析
:剑指 Offer 49. 丑数
# 具体参考动画有助于理解
a = b = c = 0
dp[n] = min(dp[a]*2, dp[b]*3, dp[c]*5)
且判断具体是那个最小,其索引(指 a,b,c)加1,如果出现一样小的情况,这时两者都要+1
:剑指 Offer 63. 股票的最大利润
# max(前n-1天的最大收益dp[n-1],今天收益-之前的最小花费)
# 这里要注意prices为空的情况
:剑指 Offer 10 - I. 斐波那契数列
:剑指 Offer 10 - II. 青蛙跳台阶问题
:剑指 Offer 42. 连续子数组的最大和
:剑指 Offer 46. 把数字翻译成字符串
:最长回文子串(实际是属于 动态规划)
# 动规:dp[i][j]表示s[i:j] 是否为回文串(储存布尔值)
# 状态转移方程: dp[i][j] = dp[i+1][j-1] and (s[i] == s[j]) 针对长度>=3
# 边界条件:长度为一时:s[i][i] = True
# 长度为二时: s[i][i+1] = (s[i] == s[i+1])
2、模板: 动规五部曲
动规五部曲:
这里我们要用一个一维dp数组来保存递归的结果
1、确定dp数组以及下标的含义
dp[i]的定义为:第i个数的斐波那契数值是dp[i]
2、确定递推公式
为什么这是一道非常简单的入门题目呢?
因为题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];
3、dp数组如何初始化(边界条件)
题目中把如何初始化也直接给我们了,如下:
dp[0] = 0;
dp[1] = 1;
4、确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
5、举例推导dp数组(检验)
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:
0 1 1 2 3 5 8 13 21 34 55
如果代码写出来,发现结果不对,就把dp数组打印出来看看和我们推导的数列是不是一致的。
6、细节:
1)dp数组分配空间的长度,例如n还是n+1 并且要考虑特殊情况如 输入长度为空的处理
2)循环的起点和终点
3)最终返回,例 dp[n] 或者是 dp[n-1],甚至max(dp)等
2、
八、综合
1、大数处理:
1、题目会规定取模1e9+7(1000000007)
xxx % 1000000007
2、正无穷:float(“inf”)
float(“inf”)表示正无穷
3、负数除法
负数除法一般题意要求向0取整
而python 从来都是向下取整,为实现向0取整,可以:
对 Python 的整数除法问题,可以用 int(num1 / float(num2)) 来做,即先用浮点数除法,然后取整。
涉及小数的除法,除数用浮点数类型可以很好的解决问题
三、递归
1、题目
:剑指 Offer 64. 求1+2+…+n
在这里插入代码片
2、模板:递归三部曲
1、确定递归函数的参数和返回值
2、确定终止条件
3、确定单层递归的逻辑
2、实例
递归三部曲
- 确定递归函数的参数和返回值
因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。
返回值自然是bool类型。
代码如下:
bool compare(TreeNode* left, TreeNode* right)
- 确定终止条件
要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。
节点为空的情况有:
左节点为空,右节点不为空,不对称,return false
左不为空,右为空,不对称 return false
左右都为空,对称,返回true
此时已经排除掉了节点为空的情况,那么剩下的就是左右节点不为空:
左右都不为空,比较节点数值,不相同就return false
此时左右节点不为空,且数值也不相同的情况我们也处理了。
代码如下:
if (left == NULL && right != NULL) return false;
else if (left != NULL && right == NULL) return false;
else if (left == NULL && right == NULL) return true;
else if (left->val != right->val) return false; // 注意这里我没有使用else
注意上面最后一种情况,我没有使用else,而是elseif, 因为我们把以上情况都排除之后,剩下的就是 左右节点都不为空,且数值相同的情况。
3)确定单层递归的逻辑
此时才进入单层递归的逻辑,单层递归的逻辑就是处理 右节点都不为空,且数值相同的情况。
比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
比较内测是否对称,传入左节点的右孩子,右节点的左孩子。
如果左右都对称就返回true ,有一侧不对称就返回false 。
代码如下:
bool outside = compare(left->left, right->right); // 左子树:左、 右子树:右
bool inside = compare(left->right, right->left); // 左子树:右、 右子树:左
bool isSame = outside && inside; // 左子树:中、 右子树:中(逻辑处理)
return isSame;
如上代码中,我们可以看出使用的遍历方式,左子树左右中,右子树右左中,所以我把这个遍历顺序也称之为“后序遍历”(尽管不是严格的后序遍历)。
四、单调栈
- 单调减栈,从栈底到栈顶,元素从大到小,
遍历数组时,如果当前元素大于栈顶,就把栈顶弹出,接着比,直到小于等于新的栈顶,压入当前元素进栈
# 单调减栈
stack = []
for i in range(len(list1)):
if stack and stack[-1] < list1[i]:
stack.pop()
stack.append(list1[i])
五、贪心算法
1、贪心一般解题步骤
将问题分解为若干个子问题
找出适合的贪心策略
求解每一个子问题的最优解
将局部最优解堆叠成全局最优解
思考过程:想清楚局部最优,想清楚全局最优,感觉局部最优是可以推出全局最优,并想不出反例,那么就试一试贪心。