013 python数据结构与算法:双向链表

    技术2022-07-10  145

    链表

    单链表,所有节点指针都指向他们的后继节点。

    后继节点:当前节点的下一个节点前驱节点:当前节点的前一个节点

    双向链表

    一种更复杂的链表是“双向链表”或“双面链表”。每个节点有两个链接:一个指向前前一个节点,当此节点为第一个节点时,指向空值;而另一个指向下一个节点,当此节点为最后一个节点时,指向空值。 需要注意的是:链表中出现的next仅仅表示“下一个”,和生成器里的next是不一样的。

    在双向链表中,头节点没有前驱,尾节点没有后继,都指向空。

    操作具体实现

    is_empty()链表是否为空length()链表长度travel()遍历链表add(item)链表头部添加append(item)链表尾部添加insert(pos,item)在指定位置添加remove(item)删除节点search(item)查找节点是否存在

    首先要先等一节点类:

    class Node(object): """节点""" def __init__(self,item): self.elem=item self.next=None self.prev=None

    双向链表的节点中有两个地址区域,一个指向节点的前驱节点,一个指向节点的后继节点。 接着要定义双向链表的类和双向链表的具体操作方法: 判断链表是否为空

    class DoubleLinkList(object): """双链表""" def __init__(self,node=None): self.__head=node def is_empty(self): """判断链表是否为空""" return self.__head is None

    求链表长度 双向链表求链表长度的操作和单链表是一样的,同样需要一个游标去进行标记、计数。

    def length(self): """求链表长度""" cur=self.__head#cur用来移动遍历节点 count=0#计数 while cur!=None: count+=1 cur=cur.next return count

    遍历链表

    def travel(self): """遍历整个链表""" cur=self.__head while cur!=None: print(cur.elem,end=" ") cur=cur.next print(" ")

    双向链表的添加元素和删除元素就会用到前驱节点和后继节点,所以对应的操作也和单链表的操作不一样。 因为以上判空、求长度和遍历都和单链表的操作一样,所以我们可以考虑继承和复用。直接继承单链表的操作,就不用再写一遍相应操作的代码,这是面向对象的继承(代码复用)。

    class DoubleLinkList(SingleLinkList): #这样去调用单链表中的一些方法就可以实现代码复用

    头插法插入元素 由图可知,头插法的代码是不唯一的,只要修改链的顺序正确就可以。 可以先改变node的next域指向self.__head,再改变self._head指向node,最后再将100所属节点的前驱节点指向node。就完成了一次头部插入节点。 代码:self.__head.prev=node self.__head=node node.next.prev=node

    def add(self,item): """链表头部插入节点""" node=Node(item) node.next=self.__head self.__head=node node.next.prev=node

    链表尾部插入节点 和头插法一样,代码是不唯一的,关键是修改链的顺序不能出错。

    def append(self,item): """链表尾部插入节点""" node=Node(item) if self.is_empty(): self.__head=None else: cur=self.__head while cur.next!=None: cur=cur.next cur.next=node#先修改尾节点的指向(由none改为指向node) node.prev=cur#再修改node的前驱节点域指向cur

    链表指定位置插入节点 因为双向链表的每个节点都可以找到它的前一个节点,所以在在双向链表中的执行位置插入不用像单链表一样需要两个游标,只需要一个游标就可以。 我们可以先让node的next指针指向cur,接着再让node的prev(前驱域)指向cur的prev。最后再将前驱的后继指向node,cur指定节点指向node。

    node.next=cur node.prev=cur.prev cur.prev.next=node cur.prev=node

    但是如果用另外一种链接方式的话,代码就会出现变化。先修改node的next域指向cur,接着让noede的前驱指向cur的前驱。此时要注意,当先改变cur的前驱指针时,就不能再通过cur的前驱去修改指定位置节点前驱的后继。这时就需要从node的前驱下手去进行指针修改。

    node.next=cur node.prev=cur.prev cur.prev=node node.prev,next=node

    指定位置添加节点完整代码:

    def insert(self,pos,item): """指定位置添加节点""" if pos<=0: self.add(item) elif pos>(self.length()-1): self.append(item) else: cur=self.__head count=0 while count<pos: count+=1 cur=cur.next #当循环推出后,cur指向pos位置 node=Node(item) node.next=cur node.prev=cur.prev cur.prev.next=node cur.prev=node

    查找 查找的操作和单链表的查找操作是一样的,直接遍历。

    def search(self,item): """查找节点是否存在""" cur=self.__head#初始化指向头节点 while cur!=None: if cur.elem==item: return True else: cur=cur.next return False

    删除指定节点 删除节点只需要先将指定节点的前驱节点的next域指向对应节点的后继节点,再将本来的后继节点的前驱域指向本来的前驱节点,这样就完成了指定节点的删除。 此时还要考虑特殊情况:当要删除的节点是头节点时,需要先判断指定的节点是否为头节点,如果是头节点,就需要让self.head指向cur的next节点,再让其前驱指向none。

    def remove(self,item): """删除节点""" cur=self.__head while cur!=None: if cur.elem==item: #先判断此节点是否为头节点 #是头节点 if cur==self.__head: self.__head=cur.next cur.next.prev=None else: cur.prev.next=cur.next cur.next.prev=cur.prev break else: cur=cur.next

    还要考虑原链表只有一个节点的情况: 只有一个节点,删除后指向none,就没有前驱节点了,所以需要一个判断:判断链表是否只有一个节点。

    def remove(self,item): """删除节点""" cur=self.__head while cur!=None: if cur.elem==item: #先判断此节点是否为头节点 #是头节点 if cur==self.__head: self.__head=cur.next if cur.next: #判断链表是否只有一个节点 cur.next.prev=None else: cur.prev.next=cur.next cur.next.prev=cur.prev break else: cur=cur.next

    if cur.next代表如果cur.next存在,返回True。再执行后继的前驱指向none。如果不存在cur.next,那么none是没有前驱节点的,不操作。 还存在指定节点是最后一个节点的情况: 如果是最后一个元素,删除后,就不会有前驱节点,不操作,所以这里还需要有一个判断:cur.next是否存在。

    def remove(self,item): """删除节点""" cur=self.__head while cur!=None: if cur.elem==item: #先判断此节点是否为头节点 #是头节点 if cur==self.__head: self.__head=cur.next if cur.next: #判断链表是否只有一个节点 cur.next.prev=None else: cur.prev.next=cur.next if cur.next: #判断是否为链表的最后一个节点 cur.next.prev=cur.prev break else: cur=cur.next

    测试双向链表的操作具体实现:

    class Node(object): """节点""" def __init__(self,item): self.elem=item self.next=None self.prev=None class DoubleLinkList(object): """双链表""" def __init__(self,node=None): self.__head=node def is_empty(self): """判断链表是否为空""" return self.__head is None def length(self): """求链表长度""" cur=self.__head#cur用来移动遍历节点 count=0#计数 while cur!=None: count+=1 cur=cur.next return count def travel(self): """遍历整个链表""" cur = self.__head while cur != None: print(cur.elem, end=" ") cur = cur.next print(" ") def add(self, item): """链表头部插入节点""" node = Node(item) node.next = self.__head self.__head = node node.next.prev = node def append(self, item): """链表尾部插入节点""" node = Node(item) if self.is_empty(): self.__head = node else: cur = self.__head while cur.next != None: cur = cur.next cur.next = node # 先修改尾节点的指向(由none改为指向node) node.prev = cur # 再修改node的前驱节点域指向cur def insert(self,pos,item): """指定位置添加节点""" if pos<=0: self.add(item) elif pos>(self.length()-1): self.append(item) else: cur=self.__head count=0 while count<pos: count+=1 cur=cur.next #当循环推出后,cur指向pos位置 node=Node(item) node.next=cur node.prev=cur.prev cur.prev.next=node cur.prev=node def search(self,item): """查找节点是否存在""" cur=self.__head#初始化指向头节点 while cur!=None: if cur.elem==item: return True else: cur=cur.next return False def remove(self,item): """删除节点""" cur=self.__head while cur!=None: if cur.elem==item: #先判断此节点是否为头节点 #是头节点 if cur==self.__head: self.__head=cur.next if cur.next: #判断链表是否只有一个节点 cur.next.prev=None else: cur.prev.next=cur.next if cur.next: cur.next.prev=cur.prev break else: cur=cur.next if __name__=="__main__": dll=DoubleLinkList() print(dll.is_empty()) print(dll.length()) dll.append(1)#1 print(dll.is_empty()) print(dll.length()) dll.append(2)#12 dll.add(8)#812 dll.append(3)#8123 dll.append(4) dll.append(5) dll.append(6)#8123456 dll.insert(-1,9)#98123456 dll.travel() dll.insert(3,100)#981 100 23456 dll.travel() dll.insert(10,200)#981 100 23456 200 dll.travel() dll.remove(100)#981 23456 200 dll.travel() dll.remove(9)#81 23456 200 dll.travel() dll.remove(200)#81 23456 dll.travel()

    运行结果:

    True 0 False 1 9 8 1 2 3 4 5 6 9 8 1 100 2 3 4 5 6 9 8 1 100 2 3 4 5 6 200 9 8 1 2 3 4 5 6 200 8 1 2 3 4 5 6 200 8 1 2 3 4 5 6

    由此可见,在链表中,由于单链表和双向链表的结构差异,操作方式有些是相同的,有些事不相同的,需要全面考虑。

    单向循环链表

    单链表的一个变形是单向循环链表,链表中最后一个节点的next域不再为none,而是指向链表的头节点。

    操作的具体实现

    is_empty()链表是否为空length()链表长度travel()遍历链表add(item)链表头部添加append(item)链表尾部添加insert(pos,item)在指定位置添加remove(item)删除节点search(item)查找节点是否存在 在定义单向循环链表的节点类时,需要注意其尾节点还要指向头节点 class SingleCircleLinkList(object): """单向循环链表""" def __init__(self,node=None): self.__head=node if node: node.next=node#尾节点要指向头节点 def is_empty(self): """链表是否为空""" return self.__head==None

    求链表长度 同样需要遍历,当遍历到next指向__head时,表示遍历结束。但是不可以使用cur==self.__head这一条件判断,应该当遍历开始的第一个节点就是self._head。 判断进入循环的条件变了,那么count的初始值就不能再是0,而是从1开始,这样才能count出正确的值。

    def length(self): """链表长度""" #cur游标,用来移动遍历节点 cur=self.__head #count记录数量 count=1 while cur.next!=self.__head: count+=1 cur=cur.next return count

    对于空链表来说,没法进行count,因此需要进行判断:

    def length(self): """链表长度""" if self.is_empty(): return 0 #cur游标,用来移动遍历节点 cur=self.__head #count记录数量 count=1 while cur.next!=self.__head: count+=1 cur=cur.next return count

    对于链表中只有一个节点也可以通过以上代码解决。 遍历链表 当遍历到尾节点时,没法进入循环,不能打印尾节点,需要单独打印cur指向的节点。同时也是需要考虑特殊情况,如链表为空的情况,链表中只有一个节点的情况:

    def travel(self): """遍历整个链表""" if self.is_empty(): return cur=self.__head while cur.next!=self.__head: print(cur.elem,end=" ") cur=cur.next #退出循环,cur指向尾节点,但尾节点的元素不打印,需要单独打印 print(cur.elem)

    头插法 在单向循环链表的头部插入节点,不仅需要像单链表一样修改两次指针,还要将尾节点的指针指向新插入的节点元素,所以单向循环链表的头插法还需要遍历。

    def add(self,item): """链表头部插入节点""" node=Node (item) cur=self.__head while cur.next!=None: cur=cur.next #退出循环,cur指向尾节点 node.next=self.__head self.__head=node cur.next=self.__head

    上述代码中,cur,next=self.__head之所以能使用,是因为在使用前,已经将self._head指向了新添加的节点。 除此之外,我们还要考虑特殊情况——链表为空时,此时,需要在代码中设置一个判断:

    if self.is_empty(): self.__head=node node.next=node

    尾插法 方法一:先让待插入节点的next域指向头节点,再让尾节点next域指向待插入节点。 方法二:先让cur的next域指向待插入节点,再让node的next域指向self.__head。 cur.next=node node.next=self.__head 其实两种方法本质是修改指针的顺序不同,代码也就不同。

    def append(self,item): """链表尾部添加节点,尾插法""" node=Node(item) if self.is_empty(): self.__head=node node.next=node else: cur=self.__head while cur.next!=self.__head: cur=cur.next node.next=self.__head cur.next=node

    再指定位置插入节点 除了头插和尾插,在中间插入节点,因为不涉及尾部和头部的变化,所以和单链表的在指定位置插入节点的操作是一样的。这里不再赘述。 查找元素 同样需要遍历,cur从头开始。根据判断条件,在查找循环到尾部时,其实没有进行比较,为了不落下尾部节点元素的比较,需要单独处理。当然,也要考虑当链表中只有一个节点的特殊情况:

    def search(self,item): """查找节点是否存在""" if self.is_empty(): return False cur=self.__head while cur.next!=self.__head: if cur.elem==item: return True else: cur=cur.next #退出循环,cur指向尾节点 if cur.elem==item: return True return False

    删除节点 当腰删除的元素不是头节点或者尾节点,那么操作和单向链表的操作是一致的。那么,如果想要删除头节点,先去找到尾节点是更快速的方式。 同时,需要考虑空链表的删除这个特殊情况,以及链表中只有一个节点的特殊情况。

    def remove(self,item): """删除节点""" if self.is_empty(): return cur=self.__head pre=None while cur.next!=self.__head: if cur.elem==item: #先判断此节点是否是头节点 if cur==self.__head: #是头节点 #找尾节点 rear=self.__head while rear.next!=self.__head: rear.next=self.__head self.__head=cur.next rear.next=self.__head else: #是中间节点 pre.next=cur.next return else: pre=cur cur=cur.next #退出循环,cur指向尾节点 if cur.elem==item: if cur==self.__head #链表只有一个节点 self.__head=None else: pre.next=cur.next

    在单链表删除元素时,通过break终止循环来终止函数,但是在单循环链表中,break终止的只是循环并没有终止函数。所以这里只能用return。 remove操作是删除元素的,需要遍历链表,首先要考虑链表是否为空:需要两个游标做辅助,遍历的终止条件是cur.next=self.head。 退出循环后,需要判断cur指向节点是否是指定节点, 当链表里只有一个节点,且这个节点恰好是要被删除的节点,这时pre指向的是none,不适用最后的pre.next=cur.next这句代码,所以需要self.head直接指向none。在单向循环链表中,删除头节点的时候,需要更改一下尾节点的next域,所以需要先找到尾节点。 测试单循环链表:

    class Node(object): """节点""" def __init__(self,item): self.elem=item self.next=None class SingleCircleLinkList(object): """单向循环链表""" def __init__(self,node=None): self.__head=node if node: node.next=node#尾节点要指向头节点 def is_empty(self): """链表是否为空""" return self.__head==None def length(self): """链表长度""" if self.is_empty(): return 0 #cur游标,用来移动遍历节点 cur=self.__head #count记录数量 count=1 while cur.next!=self.__head: count+=1 cur=cur.next return count def travel(self): """遍历整个链表""" if self.is_empty(): return cur=self.__head while cur.next!=self.__head: print(cur.elem,end=" ") cur=cur.next #退出循环,cur指向尾节点,但尾节点的元素不打印,需要单独打印 print(cur.elem) def add(self,item): """链表头部插入节点""" node=Node (item) if self.is_empty(): self.__head=node node.next=node else: cur=self.__head while cur.next!=self.__head: cur=cur.next #退出循环,cur指向尾节点 node.next=self.__head self.__head=node cur.next=self.__head def append(self,item): """链表尾部添加节点,尾插法""" node=Node(item) if self.is_empty(): self.__head=node node.next=node else: cur=self.__head while cur.next!=self.__head: cur=cur.next node.next=self.__head cur.next=node def insert(self,pos,item): """指定位置添加节点元素""" if pos<=0: self.add(item) elif pos>(self.length()-1): self.append(item) else: pre=self.__head count=0 while count<(pos-1): count+=1 pre=pre.next #当循环退出后,pre指向pos-1的位置 node=Node(item) node.next=pre.next pre.next=node def search(self,item): """查找节点是否存在""" if self.is_empty(): return False cur=self.__head while cur.next!=self.__head: if cur.elem==item: return True else: cur=cur.next #退出循环,cur指向尾节点 if cur.elem==item: return True return False def remove(self,item): """删除节点""" if self.is_empty(): return cur=self.__head pre=None while cur.next!=self.__head: if cur.elem==item: #先判断此节点是否是头节点 if cur==self.__head: #是头节点 #找尾节点 rear=self.__head while rear.next!=self.__head: rear.next=self.__head self.__head=cur.next rear.next=self.__head else: #是中间节点 pre.next=cur.next break else: pre=cur cur=cur.next #退出循环,cur指向尾节点 if cur.elem==item: if cur==self.__head: #链表只有一个节点 self.__head=None else: pre.next=cur.next if __name__ == "__main__": sll = SingleCircleLinkList() print(sll.is_empty()) print(sll.length()) sll.append(1) # 1 print(sll.is_empty()) print(sll.length()) sll.append(2) # 12 sll.add(8) # 812 sll.append(3) # 8123 sll.append(4) sll.append(5) sll.append(6) # 8123456 sll.insert(-1, 9) # 98123456 sll.travel() sll.insert(3, 100) # 981 100 23456 sll.travel() sll.insert(10, 200) # 981 100 23456 200 sll.travel() sll.remove(100) # 981 23456 200 sll.travel() sll.remove(9) # 81 23456 200 sll.travel() sll.remove(200) # 81 23456 sll.travel()

    运行结果: 链表小知识: p指针指向的这一块内存区域是可以存链表相关信息的区域(有什么节点,或者一些辅助信息)

    Processed: 0.013, SQL: 9