栈
stack是后进先出的结构,关键操作有三种:入栈push,出栈pop,和返回栈顶的元素peek
# 实现大小固定的栈结构,关键点在于size变量,栈顶指针(py不是必要的),以及存储数据的列表(数组)
# 栈顶指针指向当前栈顶元素
class Stack:
def __init__(self, size):
self.data = []
self.size = size
def push(self, data):
if len(self.data) == self.size:
raise Exception("stack is full")
self.data.append(data)
def pop(self):
if not len(self.data):
raise Exception("stack is empty")
return self.data.pop()
def peek(self):
return self.data[-1]
经典题目:LeeCode155 最小栈。思路是构建两个栈,一个正常使用,一个只存当前最小值,然后在pop操作上,两者同步弹出。
class MinStack:
def __init__(self):
"""
initialize your data structure here.
"""
self.__min_stack = []
self.__stack = []
def push(self, x: int) -> None:
self.__stack.append(x)
# 这一句是易错点!注意or的短路原则以及越界问题
m = x if not len(self.__min_stack) or x < self.__min_stack[-1] else self.__min_stack[-1]
self.__min_stack.append(m)
def pop(self) -> None:
if not len(self.__stack):
return
self.__stack.pop()
self.__min_stack.pop()
def top(self) -> int:
return self.__stack[-1]
def getMin(self) -> int:
return self.__min_stack[-1]
队列
queue是先进先出的结构,关键操作有两种,入队和出队,关键指针有两个,一个是star指向队首元素,一个end指向队尾元素。
class Queue:
def __init__(self, size):
self.size = size
self.data = []
def en_queue(self, d):
if len(self.data) >= self.size:
raise Exception("queue is full")
self.data.append(d)
def de_queue(self):
if not len(self.data):
raise Exception("queue is empty")
return self.data.pop(0)
一道经典的业务题,也是用到了双队列去解决,多队列的问题在于poll的时候,怎么确定多个队列哪个队首元素先出队,这个解决方法内部加时间戳来解决。
class PetQueue:
def __init__(self):
self.__dog_queue = []
self.__cat_queue = []
self.count = 0
def add(self, obj):
pet = (obj, self.count) # 自己加时间戳,解决poll_all的时候不知道猫和狗队列那一个先出队
self.count += 1
if obj.type == 'cat': # 默认有一个判断猫还是狗的方法
self.__cat_queue.append(pet)
else:
self.__dog_queue.append(pet)
def is_cat_empty(self):
pass
def is_dog_empty(self):
pass
def is_empty(self):
pass
def poll_dog(self):
if self.is_dog_empty():
raise Exception("dog is empty")
return self.__dog_queue.pop(0)[0]
def poll_cat(self):
if self.is_cat_empty():
raise Exception("cat is empty")
return self.__cat_queue.pop(0)[0]
def poll_all(self):
poll_list = []
while not self.is_cat_empty() and not self.is_dog_empty():
poll = self.poll_dog() if self.__dog_queue[0][1] > self.__cat_queue[0][1] else self.poll_cat()
poll_list.append(poll)
while not self.is_cat_empty():
poll_list.append(self.poll_cat())
while not self.is_dog_empty():
poll_list.append(self.poll_dog())
self.count = 0
return poll_list
关于队列和栈:有时候题目限制要用队列实现xxx,但是明显解决方案是用栈的话,可以用队列转化成栈结构。抽象出来就是如何仅用队列结构实现栈结构,如何仅用栈结构实现队列结构。思路就是双队列实现栈,假如两个队列A和B及一个数组[3,4,5],数据先填满一个队列A,然后从这个队列A到另一个队列B,留下最后一个元素5为止,这时候A只有一个5,B有3和4,把A唯一留下的元素弹出,然后重复过程,即可实现栈的后进先出。实现队列也是双栈,一个栈先装满,然后弹出到另一个栈中知道剩下第一个进栈的元素,就弹到外面,实现了先进先出。
数组问题的方法论
数组问题一般涉及一些逆序打印和转置等等,这类问题不要限于微观层面,否则代码复杂逻辑混乱。从宏观上去思考,coding会相对容易。
leecode54
# Spiral Matrix
# 给一个二维数组,实现转圈打印
# 难点是怎么转换下标,如果只思考下标转移实现,则思路复杂,应该宏观上去找到关键点
# 这道题只要找准了两个对角点,即可实现逆序下标有规律转移。
# 剩下的就是coding能力的考察,情况复杂,输入还有可能是空...很坑
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
res = []
if not len(matrix):
return res
a_x,a_y,b_x,b_y = 0,0,len(matrix[0])-1,len(matrix)-1
while a_x < b_x and a_y < b_y:
for i in range(a_x,b_x):
res.append(matrix[a_y][i])
for i in range(a_y,b_y):
res.append(matrix[i][b_x])
for i in range(b_x,a_x,-1):
res.append(matrix[b_y][i])
for i in range(b_y,a_y,-1):
res.append(matrix[i][a_x])
a_x+=1
a_y+=1
b_x-=1
b_y-=1
if a_x == b_x and a_y == b_y:
res.append(matrix[a_x][a_y])
return res
if a_x == b_x:
for i in range(a_y,b_y+1):
res.append(matrix[i][a_x])
if a_y == b_y:
for i in range(a_x,b_x+1):
res.append(matrix[a_y][i])
return res
leecode59,两题都是类似的思路,值得注意的是,两个顶点移动的时候,有可能互相越过对方的!这一点容易错误,越过考虑奇数和偶数的情况。
# 给定一个n,生成一个n²填充的转圈矩阵
class Solution:
# def generateMatrix(self, n: int) -> List[List[int]]:
def generateMatrix(self, n: int):
# 生成一个二维矩阵,最优雅的方式
res = [[0] * n for _ in range(n)]
index = 1
# 正方形,所以x和y一样
a = 0
b = n
while a < b-1: # 这个条件很重要很容易错误
for i in range(a,b):
res[a][i] = index
index += 1
for i in range(a+1, b):
res[i][b-1] = index
index += 1
for i in range(b-2, a-1, -1):
res[b-1][i] = index
index += 1
for i in range(b-2, a, -1):
res[i][a] = index
index += 1
a += 1
b -= 1
if a == b-1:
res[a][b-1] = index
return res
leecode48,正方形矩阵原地转90°
class Solution:
# def rotate(self, matrix: List[List[int]]) -> None:
def rotate(self, matrix):
"""
Do not return anything, modify matrix in-place instead.
"""
# 分治,把最外面一圈的搞定,里面也是重复的操作
# 然后在最外边的处理中,尽量抽象成重复的一个操作(类似子函数思想)
a = 0
b = len(matrix)
while a < b:
for i in range(0, b-a-1):
# 一次 swap 4个,
matrix[a][a+i], matrix[a+i][b-1], matrix[b-1][b-1-i], matrix[b-1-i][a] = \
matrix[b - 1 - i][a], matrix[a][a+i], matrix[a+i][b-1], matrix[b-1][b-1-i]
a += 1
b -= 1
这几题思路其实一样,coding能力个人理解就是指编程实现你的思路的能力,思路就是算法,就是怎么用一种计算机解决问题的思路去思考和解决问题。这里面的关键点是宏观,所谓宏观就是抽象出一种复用性好的解决方案,这样往往代码量少,思路清晰。比如上面的题目,解决外层就解决里面所有层(处理方式一样),每层中每四个对应的数又可以统一处理,就可以实现每层四个数四个数那样同样地处理。可能就是所谓的分治吧。
链表
链表的要点就两个,一个是值,一个是指针。单链表就是只有一个指针的节点组成的链表。
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None
笔试和面试中,链表的题目解决策略不一样。笔试中,链表问题应该尽快解决就行,不用考虑空间复杂度。而面试中,应该考虑不用复杂空间的解决方案,力求最优解。链表最基础的两道题,反转链表和反转双向链表之类的,不多叙述。其中关于回文串,最简单的思路是利用栈结构,把回文串一个个入栈再弹出来一个个形成逆序和原数据比较。
leecode 21 Merge Two Sorted Lists.从小到大合并两个有序链表,链表结构其实都是一个个节点组成,所以不存在新建一个链表会浪费空间什么的,这道题的思路如果陷于以前数组的思路上在原来链表上实现merge其实更复杂,如果新建一个链表一个个添加的话不会浪费额外太多的空间,反而思路简单。
leecode 234 判断回文链表
# 简单利用一个栈去解决,空间复杂度达到O(N)
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
stack = []
cur = head
while cur:
stack.append(cur.val)
cur = cur.next
while head:
if not head.val == stack.pop():
return False
head = head.next
return True
# 利用指针双向遍历,额外空间复杂度O(1)
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
stack = []
flag = True
fast = slow = head
if not head:
return flag
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
# 此时慢指针走到中点位置,此处开始反转指针
r = right = self.reverse(slow)
# 左右同时走,一边走一边对比,相遇为止
while head != right:
if not head.val == right.val:
flag = False
break
if right.next == head:
break
head = head.next
right = right.next
# 还原原来的链表
self.reverse(r,slow)
return flag
def reverse(self, head, stop=None):
next_node = head.next
while next_node != stop and next_node:
temp = next_node.next
next_node.next = head
head, next_node = next_node, temp
if stop:
stop.next = head
return head
上面这道题特别的地方在于有序,而且公共部分到底是指node的值还是完全指node也是含糊不清的。看了网上的blog,基本都是以node的值相同就是公共部分。其思路就是两个链表头从头遍历,类似于外排算法,找到值相同的那部分打印出来,有点简单就不贴代码。
leecode160返回两个单链表相交的第一个节点。最简单的思路,一个list存了一个链表所有节点,然后遍历对比,最后超时gg。链表相交的特点是两个单链表的尾节点必定一样,因此类似外排,先遍历两个单链表得到长度len_long和len_short,则两个单链表最早相交点只能是len_short处。然后长的数组先走len(long-short)步,然后开始同步走,直到找到第一个节点。顺利AC。
leecode141和142.判断单链表是否有环,有环则返回进环的第一个节点。最简单的思路,一个list存储直到找到重复节点return。进阶版是做到O(1)额外空间复杂度,具体做法是一个快指针每次走两步,一个慢指针,当快指针追上慢指针的时候就是进环节点,如果快指针指向None,则无环。
类荷兰国旗的链表问题
最简单的思路:把链表的值拷贝下来存在一个数组里面,像解决荷兰国旗问题对数组partition,最后根据数组里面的值重组链表。但是缺陷也是很明显,首先partition的过程中无法保证稳定性,其次是额外的空间复杂度。
更完美的做法:准备三个node,遍历一遍链表,找到第一个小于pivot的节点,用less节点copy这个节点,同理copy第一个等于pivot的节点eq,copy第一个大于pivot的节点more,(不存在的话赋值null,例如等于pivot的节点不存在,eq就是null)然后重新遍历链表,跳过之前遍历过的三个节点(通过对比),把小于pivot的节点挂在less上,等于pivot的节点挂在eq上,大于pivot的节点挂在more上。遍历完了之后,把less,eq以及more重组起来。(首尾相接,存首尾的节点,然后要考虑null的情况)这个解决方案更加完美,实现了稳定性以及O(1)的空间复杂度。
leecode148 sort List 更进一步,除了列表做partition之外,还用到了类似快排的思路去排序
class Solution:
def sortList(self, head: ListNode) -> ListNode:
if not head or not head.next:
return head
# 重组链表所需的首尾6个变量
eq_head = eq = None
left_head = left = None
right_head = right = None
cur = head
# 重组大 中 小 三条链子
while cur:
if cur.val < head.val:
if left:
left.next = cur
else:
left_head = cur
left = cur
elif cur.val > head.val:
if right:
right.next = cur
else:
right_head = cur
right = cur
else:
if eq:
eq.next = cur
else:
eq_head = cur
eq = cur
cur = cur.next
# 类似quick sort,递归处理小于和大于两部分,然后根据边界情况重组
if left:
left.next = None
left = left_head = self.sortList(left_head)
while left.next:
left = left.next
left.next = eq_head
else:
left_head = eq_head
if right:
right.next = None
right_head = self.sortList(right_head)
eq.next = right_head
else:
eq.next = None
return left_head
拷贝含有随机指针节点的链表。leecode138。这道题第一次思考的时候往往忽略了深度拷贝这个问题,就是说复制得到的链表是全新的链表,节点的指向关系也复制了,原来链表节点1指向节点2,返回的新链表就需要时复制节点1指向复制节点2.
第一种最简单的方案借助了散列表的数据结构,这道题的关键是复制了节点之后,怎么找到复制节点的索引,解决了这个问题其他只需要遍历复制就可以解决。利用了散列表,就可以根据原节点,通过键值对的方式找到复制节点,在python中用的是字典这个结构。code如下,(需要特别特别注意边界,也就是指向none的节点,none不能作为键,而应该直接让复制节点指向none)
class Solution:
def copyRandomList(self, head: 'Node') -> 'Node':
if not head:
return head
cur = head
node_dict = {}
while cur:
node_dict[cur] = Node(cur.val, None, None)
cur = cur.next
cur = head
while cur:
random_node, next_node = cur.random, cur.next
# random_node 或者 next_node 指向none,应该直接指向none
# 最容易忽略的边界
node_dict[cur].random = None if not cur.random else node_dict[random_node]
node_dict[cur].next = None if not cur.next else node_dict[next_node]
cur = cur.next
return node_dict[head]
第二种不借助任何数据结构的方法,解决这个问题的关键还是如何建立起原节点如何与复制节点建立索引的问题,因此,可以直接把复制节点成为原节点的next节点,然后类似上面的思路,基本是coding问题。
class Solution:
def copyRandomList(self, head: 'Node') -> 'Node':
if not head:
return head
cur = head
while cur:
cp = Node(cur.val, None, None)
next_node = cur.next
cur.next = cp
cp.next = next_node
cur = next_node
# 必须把random关系单独出来copy,否则无法保证后面的random找得到对应cp节点
cur = head
res = cur.next
while cur:
cp = cur.next
cp.random = None if not cur.random else cur.random.next
cur = cp.next
# 最后copy next节点的关系
cur = head
while cur:
cp = cur.next
next_node = cp.next
next_cp = None if not next_node else next_node.next
#
cp.next = next_cp
cur.next = next_node
cur = cur.next
return res
比较难的一类,两个单链表相交的一系列问题。如,返回两个单链表的相交的一个节点。
这里相交如果考虑了有无环的情况就逻辑很复杂了,到时再补...