为什么需要链表
顺序表的构建需要预先知道数据大小来申请连续的存储空间,而在进行扩充时又需要进行数据的搬迁,所以使用起来并不是很灵活。
链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。
链表的定义
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是不像顺序表意昂连续存储数据,而是在每一个节点(数据存储单元)里存放下一个节点的位置信息(即地址)。
单向链表
单向链表也叫单链表,是链表中最简单的一种形式,她的每个节点包含两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接域则指向一个空值。
- 表元素域elem用来存放具体的数据
- 链接域next用来存放下一个节点的位置(python中的标识)
- 变量p指向链表的头节点(首节点)的位置,从p出发能找到表中的任意节点。
节点实现
class SingleNode(object):
'''单链表的结点'''
def __init__(self,item):
# _item存放数据元素
self.item = item
# _next是下一个节点的标识
self.next = None
单链表的操作
- is_empty() 链表是否为空
- length()链表长度
- travel()遍历整个链表
- add(item)链表头部添加元素
- append(item)链表尾部添加元素
- insert(pos,item)指定位置添加元素
- remove(item)删除节点
- search(item)查找节点是否存在
单链表的实现
'''
节点实现
链表的实现
'''
class SingleNode(object):
"""单链表的节点"""
def __init__(self, item):
# item存放数据元素
self.item = item
# next是下一个节点的标识
self.next = None
# [] append empty
class SingleLinkList(object):
"""单链表"""
# 空的单链表
# 非空的单链表
def __init__(self, node=None):
self.__head = node
def is_empty(self):
"""链表是否为空"""
# if self.__head == None:
# return True
# else:
# return False
return self.__head == None
def length(self):
"""链表长度"""
# 空链表的情况下
# cur游标/指针 指向首节点 用来遍历
cur = self.__head # None
count = 0 # count = 1 正常情况下 但是,若为空链表,count会返回为1
while cur != None:
count += 1
# 将游标后移动一位
cur = cur.next
return count
def travel(self):
"""遍历整个链表"""
cur = self.__head
while cur != None:
# 节点中的数据
print(cur.item, end=' ')
cur = cur.next
print("")
def append(self, item):
"""链表尾部添加元素"""
# item 你要具体插入的数据
node = SingleNode(item)
# 考虑空链表
if self.is_empty():
self.__head = node
else :
cur = self.__head # None
# AttributeError: 'NoneType' object has no attribute 'next'
while cur.next != None:
# 找到最后一个节点
cur = cur.next
cur.next = node
def add(self, item):
"""链表头部添加元素"""
# item 你要具体插入的数据
node = SingleNode(item)
# 将新节点的链接区next指向头结点
node.next = self.__head
# 将链表的头指向新节点
self.__head = node
def insert(self, pos, item): # insert(2,100) insert(-1,100)
"""指定位置添加元素"""
# item 你要具体插入的数据
# 头部插入
if pos <= 0:
self.add(item)
# 尾部插入
elif pos > self.length()-1:
self.append(item)
else:
# 指定位置插入
node = SingleNode(item)
pre = self.__head
count = 0
while count < (pos - 1):
count += 1
pre = pre.next
node.next = pre.next
pre.next = node
def remove(self, item):
"""删除节点"""
# remove(100) [100,200,300] 删除一个100
cur = self.__head # 空链表 None
pre = None
while cur != None:
# 找到了指定E的元素
if cur.item == item:
# 删除第一个元素
if cur == self.__head:
self.__head = cur.next
else:
# 删除
pre.next = cur.next
break
else:
# 继续移动
pre = cur
cur = cur.next
def search(self, item):
"""查找节点是否存在"""
cur = self.__head
while cur != None:
if cur.item == item:
return
else:
# 让游标继续执行
cur = cur.next
return False
# 有一个节点,存放的数据是100
# node = SingleNode(100)
# 测试
s = SingleLinkList()
# s.length()
print(s.is_empty())
print(s.length())
s.append(1)
s.append(3)
s.append(5)
s.append(8)
s.append(100)
print(s.is_empty())
print(s.length())
# s.travel()
s.add(10)
s.travel()
s.insert(-1, 666)
s.insert(10, 777)
s.insert(2, 888)
s.travel()
print(s.search(777))
s.remove(888)
s.travel()
print(id(s))
链表与顺序表的对比
链表失去了顺序表随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大,但对存储空间的使用要相对灵活。
操作 | 链表 | 顺序表 |
---|---|---|
访问元素 | O(n) | O(1) |
在头部插入/删除 | O(1) | O(n) |
在尾部插入/删除 | O(n) | O(1) |
在中间插入/删除 | O(n) | O(n) |
注意虽然表面看起来复杂度都是O(n),但是链表和顺序表在插入和删除时进行的是完全不同的操作。链表的主要耗时操作是遍历查找,删除和插入操作本身的复杂度O(1)。顺序白哦查找很快,主要耗时的操作是拷贝覆盖。因为除了目标元素在尾部的特殊情况,顺序表进行插入和删除时需要对操作点之后的所有元素进行前后移位操作,只能通过拷贝和覆盖的方法进行。
写链表代码建议
- 理解指针或引用的含义
- 警惕指针丢失
- 重点留意边界条件处理
- 举例画图,辅助思考
如何实现LRU缓存淘汰算法?
缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非常广泛的应用,比如常见的CPU缓存、数据库缓存、浏览器缓存等等。
缓存的大小有限,当缓存被用满时,哪些数据应该被清理出去,哪些数据应该被保留?这就需要缓存淘汰策略来决定。常见的策略有三种:
先进先出策略FIFO(First In,First Out)、最少使用策略LFU(Least Frequently Used)、最近最少使用策略LRU(Least Recently Used)。
有这么一个思路:我们维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,我们从链表头开始顺序遍历链表。
1.如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
2.如果此数据没有在缓存链表中,又可以分为两种情况:
- 如果此时缓存未满,则将此结点直接插入到链表de头部;
- 如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。