终于开始提笔写算法(2)了,我先为自己鼓个小手,然后决定,这一波写个常见的数据结构,链表,还望各位小伙伴多多支持,有简书号的就帮忙捧个人场,点个赞留个言啥的,你们的关注就是我继续写下去的动力~
上一篇递归写了三天,这一波链表,估计时间也不会短,,,
- 目录:
算法:附录
算法(1):递归
算法(2):链表
算法(3):数组
算法(4):字符串
算法(5):二叉树
算法(6):二叉查找树
算法(7):队列和堆栈(附赠BFS和DFS)
算法(8):动态规划
算法(9):哈希表
算法(10):排序
算法(11):回溯法
算法(12):位操作
数组我想大家都很熟悉,链表和数组都属于线性数据结构,但是长相差别很大,具体长啥样请看下图:
单向链表
链表的每一个元素存储空间不是相邻的,而数组则是存放在同一块相邻的空间下。如上图所示,链表每一个元素(后面会称之为节点)分两部分,一部分存放值(value),另一部分存放下一个元素的地址(reference field)。
上图为单向链表(singly linked list),还有一种链表,叫双向链表(doubly linked list),其结构如下:
双向链表
单向链表(Singly Linked List):
首先,我先给大家看一段用 python 定义链表的代码,很简单,简单到你可能只会瞟一眼便略过:
class ListNode: #定义节点类
def __init__(self, x):
self.val = x
self.next = None
在大部分情况里,我们一般使用头节点(head node)来表示整个链表。也正是由于链表的这个特性,我们不可能在常数时间里访问链表当中的某个随机元素,因为我们要从头节点遍历下来才行,所以查找元素所用时间复杂度为。虽然查找的时候和数组相比(数组时间复杂度为)弱爆了,但是链表自有其可取之处,比如当你插入和删除元素时,你会看到,链表正在将数组按在地上摩擦。
插入元素:
插入操作主要分为三步:首先,构建好一个节点,其次,将当前节点指向下一个节点,最后,将先前的节点指向新构建的节点。
1.png 2.png 3.png
插入一个元素时间复杂度只需要,数组则是,毕竟他要让排在后面的元素一个个给新元素挪位置。但是这里其实有一个问题,一般情况下,我们要找到这个要插入的位置再执行插入操作,而该查找操作,时间复杂度却是。比如,我给一个要求,让你在头节点新增一个元素,那很简单,直接新建节点,指向头节点,再将该新节点设置为头节点即可,很明显,这是。我看你完成的这么轻松,就使坏了,让你在链表末端插入一个节点。这个时候,你只能从头节点开始,不停的 next 到下一节点,在经过了 时间后,你终于来到了链表的尾端,感叹道:“今天晚上就要去看惊奇队长电影了!”
以免大家看了我上面的分析陷入迷糊,这里再说一下,一般我们说插入的时候,不考虑那么多,是默认已经给出插入位置了的,所以记住这句话,单向链表插入一个元素的时间复杂度为。
删除操作:
删除的时候只需要一步,被删节点的前一个节点,指向被删节点的后一个节点即可~
1.png 2.png
那么删除一个元素的时间复杂度是多少呢?。假设我们现在知道被删节点 (cur node),那么找下一个节点(next node)很简单,但是如何找它之前的那个节点(prev node)呢?答案是从头遍历。。。。这样子,虽然删除操作简单,但是为了找到 pred node,平均时间复杂度就又到了级别。
所以再记住这句话,单向链表删除一个元素的时间复杂度为。
当然还是老样子,没有 coding 的算法讲解都是耍流氓!我虽然是个流氓,例题是肯定得放上它三五个,才像回事。
那么,接下来便是代码时刻(coding time)!
问题1:判断一个链表是否有环
何为环?首尾相连即为环。请看下图:
调皮的同学可能会说,你这玩意儿不是首尾相连,
首先,我跟大家讲个龟兔赛跑的故事。话说有一天,小乌龟和小白兔在一条笔直的公路上赛跑,
双指针,便是如此,有两个指针,一个运动速度快,一个运动速度慢,如果链表无环,则快指针先到达链表尾端;如果链表有环,则两个指针必会碰头,快指针超慢指针一圈或N圈。在一般情况下,我们设定慢指针一次一步,
slow_pointer = slow_pointer.next
;快指针一次两步,fast_pointer = fast_pointer.next.next
。那么,了解了双指针技术后,习题走起!
输入:链表
输出:True(有环) or False(无环)
代码如下:
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None
def hasCycle(head):
"""
:type head: ListNode
:rtype: bool
"""
if head == None: return False
fast = slow = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
else: return False
if __name__ == '__main__':
head1 = node1 = ListNode(1) # 无环链表头节点
head2 = node2 = ListNode(1) # 有环链表头节点
for i in range(2,5):
node1.next = ListNode(i)
node2.next = ListNode(i)
node1 = node1.next
node2 = node2.next
node2.next = head2.next # 加个环
ans1 = hasCycle(head1)
ans2 = hasCycle(head2)
print(ans1,ans2)
问题2:该问题为问题1的延申,如果链表有环,则返回环开始的那个节点。
例子:
输入:1->2->3->4(->2>3->4......)
输出: ListNode(2)
解决思路:
该问题我们仍继续用双指针的方法来做,判断有没有环很简单,但是如何找到环开始的节点呢(突然想到了万恶之源这个词...)?操作很简单,当快慢两指针相遇时,再让一个指针从头节点开始,和慢指针一同以相同步伐运动(这是快指针已经没用了),当新指针和慢指针相遇时,该位置便为所求之点!
那么到底是为啥呢?各位看官请准备好瓜子花生小板凳,听我慢慢道来:
当快慢指针碰头时,咱假设慢指针走了K步,那么快指针便走了2K步。此时快指针比慢指针多运动n个环的距离,我们假设一个环有节点s个,那么可以得到式子:,即。啥意思呢,也就是说,这个时候,慢指针走的步数刚好也是该环的整数倍!这个时候,我们搞一个和慢指针一样速度的指针(暂且称之为小白兔把)从头节点开始走,最终小白兔在走了K步时,和慢指针一定会又在该地相遇(加上之前的,此时慢指针共走了2K步)。但这不是它们首次相遇之地,它们因为速度一样,所以是在环开始的地方相遇,然后肩并肩手挽手的走到了第K个节点这里~(不信你让它们在这个第K个节点开始原路返回,你发现它们先是一起倒着走,在某个节点分道扬镳,小白兔继续朝头节点方向倒着走,而慢指针则回到了该链表尾部,继续它的绕圈圈)。
so,got it?懂了的话就来看代码把~
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None
def detectCycle( head):
"""
:type head: ListNode
:rtype: ListNode
"""
fast = slow = head
while fast and fast.next:
fast = fast.next.next
slow = slow.next
if fast == slow:
break
else:
return None
while head != slow:
head = head.next
slow = slow.next
return slow
if __name__ == '__main__':
head1 = node1 = ListNode(1) # 无环链表头节点
head2 = node2 = ListNode(1) # 有环链表头节点
for i in range(2,5):
node1.next = ListNode(i)
node2.next = ListNode(i)
node1 = node1.next
node2 = node2.next
node2.next = head2.next # 加个环
ans1 = detectCycle(head1)
ans2 = detectCycle(head2)
print(ans1)
print(ans2.val)
问题3:这个问题还需要我们的双指针大侠出手。如下图所示,给两个连在一起的链表,我们需要找到这个连接点,即c1。
看到这个问题,客官老爷不知道有没有想法?
但是,它们会绕多少圈才能首次相遇呢?有可能是很多圈。那么,有没有其他优化方法呢?(这时你又要跃跃欲试,想要发言,我赶快打断了你,继续讲下去)当然有!方法便是指针a在遍历完A链表之后,不从a1节点继续绕圈,而是跳到B链表头部b1,作为新的征程!(指针b亦然,第二圈从a1开始)这时你会发现,在第二次遍历时,他们必定会在c1处相遇。这时,每个指针都刚好把A和B所有不重复的节点走了一遍。
接下来,上代码!(功夫不负有心人,你看到机会终于来了,憋了半天的话脱口而出:“瓜子吃完了,再给我端一盘。”)
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None
def printListNode(node): #辅助函数,打印链表
while node:
print(node.val, end=' ')
if node.next:
print('->', end=' ')
node = node.next
print()
def getIntersectionNode( headA, headB):
"""
:type headA, headB: ListNode
:rtype: ListNode
"""
pa = headA
pb = headB
while pa != pb:
pa = pa.next if pa != None else headB
pb = pb.next if pb != None else headA
return pa
if __name__ == '__main__':
head1 = node1 = ListNode(1)
head2 = node2 = ListNode(1)
node1.next = ListNode(2);node1 = node1.next
node2.next = ListNode(2);node2 = node2.next
node2.next = ListNode(3);node2 = node2.next
node1.next = node2.next = ListNode(5);node1 = node1.next;node2 = node2.next
for i in range(10,12):
node1.next = ListNode(i)
node1 = node1.next
# node2 = node2.next
printListNode(head1)
printListNode(head2)
ans1 = getIntersectionNode(head1,head2)
print(ans1.val)
问题4:翻转链表。上一章我们翻转了字符串,这一章,链表羡慕的紧,也想翻一翻。那咱就给它个机会~
翻转列表有多种方法,遍历列表,把每个节点装入列表当中,然后再反向拼接这些节点便可轻松做到。但是这里的空间复杂度应该是(如果小编没理解错的话,设链表长度为n,装入第一个节点时相当于装了整个链表,占用空间n,第二个n-1......最后1,共占用个空间,但是不确定这些中间节点是否是共享的,如果共享,则为复杂度),但这里说一个复杂度没那么高的,空间复杂度为、时间复杂度为的方法,供大家参考。
如下图所示,我们的做法是从头节点开始,依次取出头节点后面的节点,放到头节点位置。第一步,先将头节点23放到头节点位置(其实就是不做任何操作......),第二步,将节点6扣下来,然后连接23与15,最后将6放在头节点位置;第三步,将15扣下来,然后连接23与None,最后将15放到头节点位置。就这么循环到链表尾部,就结束啦~
当然,代码还是得拉出来溜溜才行~
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None
def printListNode(node): #辅助函数,打印链表
while node:
print(node.val, end=' ')
if node.next:
print('->', end=' ')
node = node.next
print()
def reverseList(head):
"""
:type head: ListNode
:rtype: ListNode
"""
if head == None: return
node = head
while node.next:
cur = node.next # cur 即为被抠出来的节点
node.next = node.next.next # 将 node 的下一个节点指向更后面的那个
cur.next = head # 将 cur 跟链表拼接起来
head = cur # 保存头节点信息
return head
if __name__ == '__main__':
head1 = node1 = ListNode(1)
for i in range(2,6):
node1.next = ListNode(i)
node1 = node1.next
printListNode(head1)
ans = reverseList(head1)
printListNode(ans)
问题5:奇偶链表
输入一个链表,将第奇数个节点取出放在一起,第偶数个节点取出放在一起,然后偶数组接在奇数组后面,返回该合成后的链表。
例子:
输入:1 -> 2 -> 4 -> 6 -> 8
输出:1 -> 4 -> 8 -> 2 -> 6
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None
def printListNode(node): #辅助函数,打印链表
while node:
print(node.val, end=' ')
if node.next:
print('->', end=' ')
node = node.next
print()
def oddEvenList(head: ListNode) -> ListNode:
if head:
odd = head
even = evenHead = odd.next
while even and even.next:
odd.next = odd.next.next;
odd = odd.next;
even.next = even.next.next;
even = even.next;
# odd.next = odd = even.next #也可以试试把上面四句换成下面两句,结果是一样的呦~
# even.next = even = odd.next
odd.next = evenHead
return head
if __name__ == '__main__':
head1 = node1 = ListNode(1)
for i in range(2,10,2):
node1.next = ListNode(i)
node1 = node1.next
printListNode(head1)
ans = oddEvenList(head1)
printListNode(ans)
小贴士:
1. 在你调用一个节点的 next 操作时,要首先确认该节点是不是空节点。即if node: node = node.next
,或者if node and node.next: node = node.next.next
。
2. 要考虑循环的边界条件,不要让调用陷入死循环当中。诸如一个有环链表,当你遍历打印该链表的节点时,要写好跳出循环的条件。
3. 可以尝试同时使用多个指针(pointer)来追踪节点。
4. 很多场景下,可能需要追踪当前节点的前一个节点(previous node)。
5. 有时候指针来回交换几次之后比较迷,可以自己准备几个链表样本来测试,查看指针变换后的结果。(如上面程序中的ListNode()函数以及printListNode()函数,都可以方便我进行测试)
(还没写完,大家可以再准备点瓜子,我才刚刚说完单向链表,,,)
双向链表(Doubly Linked List):
大家还记得单向链表的定义吗?你说我没讲?我一套组合升龙拳......跳个舞给你们看......双向链表跟它相同,但是每个节点都多了一部分,一个储存前一个节点位置的空间(reference field)。具体案例如下图所示:
具体代码定义如下所示:
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None
self.prev = None # 其实没啥,就多了行这(请不要质疑我的注释水平)
双向和单向链表在遍历、查找、添加节点时,时空复杂度都是一样的,唯独删除节点,时间复杂度由 降低至 (因为可以直接找到 prev node)。内容较为简单,后面不会细说,大家如果有什么不懂的,你自己解决啊!, 热烈欢迎留言讨论~ 小编可是极其期待有人给我留言的(苦逼小编写了快二十篇文章,至今还未打破 0 评论惨案)~
添加节点:
双向链表添加节点操作和单向链表很相似,只不过原来是断开一个连接、新增两个连接,变成了断开两个连接、新增四个连接罢了~ 来,让小编我找个例子逗大伙乐乐~
下图所示,共执行两步操作:Step1.新增节点9,将该节点的 next 和 prev分别连上节点15 和节点6。Step2.断开节点6和节点15的连接,让这两个节点分别与节点9相恋。
删除节点:
那啥,删除操作非常简答,cur.prev.next = cur.next; cur.next.prev = cur.prev
,只需要更改两个连接即可~
*例题:展平多级双向链表
该双向链表多了一个功能,叫子节点(child node)。而本题的目标便是让子链表插入主链表,将链表平铺。如下例子所示,节点3和8都有自己的子节点,而我们要做的就是将节点11、12插入节点8和9之间,再将7、8、11、12、9、10插入节点2和3之间,得到一个展平的双向链表。
输入:
1---2---3---4---5---6--NULL
|
7---8---9---10--NULL
|
11--12--NULL
输出:
1-2-3-7-8-11-12-9-10-4-5-6-NULL
实现代码如下(这里使用了深度优先搜素思想(DFS),我会在后面的算法系列讲到该知识点,如果我不坑的话,后面DFS、BFS、回溯、动规等系列算法应该都会给各位看官一一奉上):
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None
self.prev = None
self.child = None
def printListNode(node): #辅助函数,打印链表
while node:
print(node.val, end=' <-> ')
if node.child:
print('(', end=' ')
tmp = node.child
while tmp:
print(tmp.val, end=' <-> ')
tmp = tmp.next
print('None) <->', end=' ')
node = node.next
print('None')
def flatten(head):
def dfs(node):
if not node:
return None, None
trav = node
head = node
tail = node
while trav:
if trav.child:
nex = trav.next
insert, last = dfs(trav.child)
trav.next = insert
insert.prev = trav
if nex:
last.next = nex
nex.prev = last
trav.child = None
trav = last
tail = trav
trav = trav.next
return head, tail
return dfs(head)
if __name__ == '__main__':
head1 = node = ListNode(1)
for i in range(5,15,2):
node.next = ListNode(i)
node.next.prev = node
if i % 3 == 0:
node.child = ListNode(i + 0.1)
node.child.prev = node
node.child.next = ListNode(i + 0.2)
node.child.next.prev = node.child
node = node.next
printListNode(head1)
head,tail = flatten(head1)
printListNode(head)
附录:在LeetCode上找到了一张好图,贴在这里,侵删。
从上图可以看出,如果你频繁添加或者删除节点,那么可以使用链表结构;如果你倾向于通过索引(index)查找元素,那么数组效果会更好~
震惊!二十来岁妙龄小伙竟然在电脑前做那种事!
小伙竟然在电脑前不停求赞求赞求赞!
网友评论