最近在链表的学习上苦战,走了不少弯路,发现《数据结构与算法-Python语言实现》这本经典书籍中,介绍的关于链表类的构造十分抽象,难以理解(可能我比较菜哈哈哈)。为了更好地掌握链表的构造和应用,我查阅其他书籍,才勉强能将单链表、循环链表、双向链表、位置信息列表等的类构造出来。这里我和大家分享一下链表中常用的带头哨兵和尾哨兵的双向链表类,以及如何使用该类实现洗牌算法。不足之处可留言点评。
关于单链表等的知识这里不再赘述,下面简单介绍哨兵和双端列表。
哨兵,也称为哨兵节点,是处于链表头部或者尾部的空节点。使用哨兵可以极大地简化操作逻辑,并且在使用链表类方法时,头尾哨兵都是不变的,改变的是头哨兵和尾哨兵之间的节点。双端链表的每个节点都带有两个指针,一个是next指针,指向下一个节点,一个是prev指针,指向上一个节点。头尾哨兵也是含有两个指针,只不过它们携带元素值是None,然后各有一侧的指针指向的是None。
首先在设计时,我们需要定义节点类Node,方便我们接下来加入节点:
class Node:
def __init__(self,item):
self.item = item #item为节点携带的元素值
self.prev = None #prev指针
self.next = None #next指针
然后设计双端链表类DouLink,在构造函数里面需要提前定义头哨兵和尾哨兵,以及他们之间的连接:
class DouLink:
def __init__(self):
self._header = Node(None) #头哨兵节点
self._tailer = Node(None) #尾哨兵节点
self._header.next = self._tailer #节点连接
self._tailer.prev = self._header
赋值时如果觉得乱,就按照指针方向顺序赋值,例如:self._header.next = self._tailer就表示头哨兵的next指向了整个尾哨兵,原先的赋值语句是右运算,为了方便理解我们可以读成左运算,但实际上Python的赋值是右运算。
构造函数里面就设计出一个空的双端列表,仅仅只是两个None节点连在一个,即头哨兵和尾哨兵。接下来设计is_empty函数和length函数,用于判断链表是否为空和求取链表长度。
"""空判断函数"""
def is_empty(self):
return self._header.next == self._tailer #仅包含头尾哨兵
"""求取链表长度函数"""
def length(self):
cur = self._header
num = 0
if self.is_empty():
num = 0
else:
while cur.next != self._tailer:
num = num+1
cur = cur.next
return num
接着是遍历链表的函数,作用是将一整个链表打印出来:
"""遍历链表函数"""
def travel(self):
cur = self._header
cur = cur.next #第一个节点(索引为0)
while cur.next != None:
print(cur.item) #逐项打印
cur =cur.next
print(" ")
设置头部添加函数和尾部添加函数,作用是在头部和尾部添加元素值为item的节点:
"""头部添加函数"""
def headadd(self,item):
node = Node(item)
if self.is_empty(): #如果链表为空,则是在头尾哨兵之间添加节点
self._header.next= node
node.prev = self._header
node.next = self._tailer
self._tailer.prev = node
else:
node.next = self._header.next #如果不为空,则是在头哨兵和第一节点之间添加节点
self._header.next.prev = node
self._header.next = node
node.prev =self._header
"""尾部添加函数"""
def tailadd(self,item): #尾哨兵前添加节点,需要将节点索引到尾哨兵之前
node = Node(item)
cur = self._header
if self.is_empty(): #如果是空链表,那么添加方式同headadd的第一种情况
self._header.next= node #节点连接
node.prev = self._header
node.next = self._tailer
self._tailer.prev = node
else:
while cur.next != self._tailer: #如果不是空链表,那么将索引引到最后一个节点,尾哨兵之前
cur=cur.next
node.next = self._tailer #节点连接
self._tailer.prev = node
cur.next = node
node.prev = cur
其中在while循环中,cur.next != self._tailer也可写成cur.next != None,结果是一样的,这更能体现哨兵是含有指针但是不具备元素值的边界节点。
接下来是插入函数insert,它包含两个参数,一个是插入节点期待的索引(或者位置)position,一个是插入节点携带的元素值item,设计思路与头添加和尾添加的思路一样:
"""插入函数"""
def insert(self,position,item):
node = Node(item)
cur = self._header
num = 0
if position <= 0 : #如果期待索引小于0,调用头添加函数
self.headadd(item)
elif position >= (self.length()-1): #如果期待索引大于长度值,调用尾添加函数
self.tailadd(item)
else:
while cur.next != None: #一般情况,引到期望索引位置,跳出循环
cur = cur.next
num = num+1
if num == position:
break
cur.next.prev = node #节点连接
node.next = cur.next
cur.next = node
node.prev = cur
为了能够实时移除链表中的某些元素,我设置了两种函数用于实时移除,一个是removepos函数,按照输入参数position(位置)删除某个索引的节点,返回新的链表;一个是removeitem函数,按照输入参数item删除链表中携带元素值为item的节点,返回新的链表。设计如下:
"""按位置移除函数"""
def removepos(self,position):
cur = self._header
cur = cur.next
num = 0
if position == 0: #0索引时移除
self._header.next = cur.next
cur.next.prev = self._header
elif position == (self.length()-1): #末端索引时移除
while cur.next != self._tailer.prev:
cur = cur.next
cur.next = self._tailer
self._tailer.prev = cur
elif position > 0 and position < (self.length()-1): #一般情况
while cur.next != None:
cur = cur.next
num = num + 1
if num == (position-1):
break
cur.next = cur.next.next
cur.next.prev = cur
else:
print("Position error!")
"""按元素值移除函数"""
def removeitem(self,item):
cur = self._header
while cur.next != None:
sin = cur.next.item
cur = cur.next
if sin == item:
break
cur.prev.next = cur.next
cur.next.prev = cur.prev
同样,我们也可以设计获取索引或者获取元素值的函数,用于对链表的内容的观测,对不满意的元素还可以实时更新,因此设计交换节点函数,设计如下:
"""索引获取函数"""
def search(self,item):
num = 0
cur = self._header
while cur.next != None: #采用while-break方式获取目标item所在的索引num
sin = cur.next.item
num = num +1
cur = cur.next
if sin == item:
break
num = num -1
return num
"""元素获取函数"""
def get(self,position):
num = 0
cur = self._header
cur = cur.next
if self.is_empty(): #空链表返回Nothing
print("Nothing.")
elif position == 0 : #获取第一节点携带的元素值
sin = self._header.next.item
elif position == (self.length()-1): #获取最后一个节点携带的元素值
sin = self._tailer.prev.item
else: #一般情况
while cur.next != None:
cur = cur.next
num = num+1
if num == position:
sin = cur.item
break
return sin
"""节点交换函数"""
def exchange(self,position,item):
if position < 0 or position > (self.length()-1): #超出范围返回Error
print("Error.")
else: #一般情况,调用removepos和insert函数,先除后插
self.removepos(position)
self.insert(position,item)
最后要设计的函数是平分函数halfdivide,将链表平分为两部分,在以后的链表操作,包括接下来要介绍的洗牌算法中,都要用到这个函数。设计时,要分类讨论链表长度值的奇偶性,然后在中间两个节点加入头哨兵和尾哨兵即可。调用这个方法时,输入参数是choice,选择返回前半段还是后半段。如果要考虑保持原链表不变,需要提前拷贝,为此设置了拷贝函数copylink:
"""拷贝函数"""
def copylink(self):
cur = self._header
cur = cur.next
copy = DouLink() #设置一个空链表
while cur.next != None: #将原链表的元素值拷贝到空链表中去
copy.tailadd(cur.item)
cur =cur.next
return copy
"""平分函数"""
def halfdivide(self,choice):
long = self.length() #获取链表长度
headsoldier = Node(None) #设置要插入的头哨兵
tailsoldier = Node(None) #设置要插入的尾哨兵
cur = self._header
cur=cur.next
cos = self._header #头哨兵拷贝备用
cos = cos.next #第一节点拷贝备用
num = 0
if self.is_empty(): #判断为空链表,返回Error
print("Error.")
elif long%2 == 0 : #获取偶数长度时中间节点索引
halfhead = long/2
else:
halfhead = (long-1)/2 #获取奇数长度时中间节点索引
halftail = halfhead-1 #另一个节点的索引是中间节点索引值减1
while cur.next != None:
cur=cur.next
num = num+1
if num == halftail:
break
headsoldier.next = cur.next #在中间插入头哨兵和尾哨兵,将两部分分开来
cur.next.prev = headsoldier
cur.next = tailsoldier
tailsoldier.prev = cur
con1 = DouLink()
con2 = DouLink() #定义两个空链表用于装载新的链表
while cos.next!=None:
con1.tailadd(cos.item) #依次从尾部
cos=cos.next
sin = headsoldier
sin = sin.next
while sin.next!=None:
con2.tailadd(sin.item)
sin = sin.next
if choice == 'before': #分段选择
return con1
elif choice == 'after':
return con2
else: #恶意输入返回Error
print("Error.")
至此,双向链表类的所有方法定义完毕。接下来需要验证一下该类方法是否能够实现功能,主函数里面测试:
if __name__ == '__main__':
dlink = DouLink()
dlink.headadd(1)
dlink.headadd(3)
dlink.headadd(4)
dlink.tailadd(8)
dlink.tailadd(9)
dlink.insert(2,6)
dlink.insert(10,11)
print("原链表:")
dlink.travel()
print("Length:",dlink.length())
dlink.removepos(3)
#dlink.removepos(4)
print("移除第三个索引节点后的链表:")
dlink.travel()
s=dlink.length()
print("Length:",s)
dlink.removeitem(9)
#dlink.removeitem(4)
print("移除元素为9的节点后的链表:")
dlink.travel()
s2=dlink.length()
print("拷贝此时的链表:")
dlink.copylink()
print("Length:",s2)
c = dlink.search(11)
print("元素11所在的索引:",c)
f = dlink.get(0)
f1 = dlink.get(3)
print("0索引和3索引的元素为:",f,f1)
print("\n")
dlink.exchange(0,7)
dlink.exchange(2,5)
print("执行交换函数后的链表:")
dlink.travel()
print("Length:",dlink.length())
dlink.headadd(2)
dlink.tailadd(1)
print("新修改的链表:")
dlink.travel()
sg = dlink.halfdivide('after')
print("平分后的链表后半部分:")
sg.travel()
输出结果如下,可以看出,双端链表类的各个方法输出结果是符合预想的:
原链表:
4
3
6
1
8
9
11 #原链表:4,3,6,1,8,9,11
Length: 7
移除第三个索引节点后的链表:
4
3
6
8
9
11 #此时链表:4,3,6,8,9,11
Length: 6
移除元素为9的节点后的链表:
4
3
6
8
11 #此时链表:4,3,6,8,11
拷贝此时的链表:
4
3
6
8
11 #此时链表:4,3,6,8,11
Length: 5
元素11所在的索引: 4
0索引和3索引的元素为: 4 8
执行交换函数后的链表:
7
3
5
8
11 #此时链表:7,3,5,8,11
Length: 5
新修改的链表:
2
7
3
5
8
11
1 #此时链表:2,7,3,5,8,11,1
平分后的链表后半部分:
5
8
11
1 #此时链表:5,8,11,1
若有不足或者更好的思路,可在评论区提出,下一篇将介绍利用该链表完成洗牌算法的实现。多谢您的过目。
带头哨兵和尾哨兵的双端链表py文件下载:
链接:https://pan.baidu.com/s/13gTzIsaB0nimI5tFWzliLg
提取码:pujs