前言
之前写了一篇《没有指针,Python如何实现链表、二叉树这些数据结构》然后有大佬觉得我那里面讲的是列表,不是链表。当时我也很疑惑,因为我也不太确定列表和链表到底有什么区别,说实话,列表确实比较好用,因为是Python已经封装好了的,方法种类多样,更加的实用。但是这难道就意味着链表这一数据结构没有丝毫的用处嘛?当然不是,只是可能你还没有到非用他不可的地步罢了。
列表详解
列表的实现机制
Python标准类型list就是一种元素个数可变的线性表,可以加入和删除元素,并在各种操作中维持已有元素的顺序(即保序),而且还具有以下行为特征:
基于下标(位置)的高效元素访问和更新,时间复杂度应该是O(1);
为满足该特征,应该采用顺序表技术,表中元素保存在一块连续的存储区中。
允许任意加入元素,而且在不断加入元素的过程中,表对象的标识(函数id得到的值)不变。
为满足该特征,就必须能更换元素存储区,并且为保证更换存储区时list对象的标识id不变,只能采用分离式实现技术。
在Python的官方实现中,list就是一种采用分离式技术实现的动态顺序表。这就是为什么用list.append(x) (或 list.insert(len(list), x),即尾部插入)比在指定位置插入元素效率高的原因。
在Python的官方实现中,list实现采用了如下的策略:在建立空表(或者很小的表)时,系统分配一块能容纳8个元素的存储区;在执行插入操作(insert或append)时,如果元素存储区满就换一块4倍大的存储区。但如果此时的表已经很大(目前的阀值为50000),则改变策略,采用加一倍的方法。引入这种改变策略的方式,是为了避免出现过多空闲的存储位置。
翻阅了多方的资料,看了其他一些大佬发的东西,我在这里总结一下:
列表的实现可以是数组或者链表。并且通过前面的学习我们知道,列表是一种顺序表,顺序表一般是数组。列表是一个线性表,它允许用户在任何位置进行插入、删除、访问和替换元素。
列表的实现是基于数组或者基于链表结构,当使用列表迭代器的时候,双向链表结构比单链表结构更快。
Python中的列表英文名是list,因此很容易与C语言中的链表搞混了,因为在C语言中大家经常给链表命名为list。事实上CPython(CPython是指用C语言实现的Python,也是我们常见的用C语言开发的Python解释器,大家应该都知道,Python语言底层是C语言实现的)中的列表根本不是列表。在CPython中列表被实现为长度可变的数组。
从细节上看,Python中的列表是由其他对象的引用组成的连续数组,指向这个数组的指针及其长度被保存在一个列表头结构中。这就意味着,每次添加或删除一个元素时,由引用组成的数组需要改变大小(重新分配)。幸运的是,Python在创建这些数组时采用了指数分配,所以并不是每次操作都要改变数组的大小。但是,也因为这个原因添加或者取出元素是平摊复杂度较低。不幸的是,在普通链表上“代价很小”的其他一些操作在Python中计算复杂度相对较高。
总的来说,Python中的列表是一个动态的顺序表,而顺序表大多是由数组实现的。
链表
Python链表的具体实现在我上面那篇文章里面介绍过了。我这里就再来炒个剩饭。
链表是由许多相同数据类型的数据项按照特定的顺序排列而成的线性表。链表中的数据项咋计算机的内存中的位置是不连续且随机的,然而列表是连续的。链表数据的插入和删除是很方便的,但数据的查找效率较低,不能像列表一样随机读取数据。
链表由一个一个的结点构成,每个结点由数据域和“指针域”组成,数据域存储数字,“指针域”指向下一个结点所在的内存地址。(因为Python中没有指针这一概念的,这里的指针是一种指向)
class Node(object):
"""节点"""
def __init__(self, elem):
self.elem = elem
self.next = None
链表封装的一系列操作:
class SingleLinkList(object):
"""单链表"""
def __init__(self, node=None):
self.__head = node
def is_empty(self):
"""链表是否为空"""
return self.__head == None
def length(self):
"""链表长度"""
# cur游标,用来移动遍历节点
cur = self.__head
# count记录数量
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
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
def insert(self, pos, item):
"""指定位置添加元素
:param pos 从0开始
"""
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 remove(self, item):
"""删除节点"""
cur = self.__head
pre = None
while cur != None:
if cur.elem == 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.elem == item:
return True
else:
cur = cur.next
return False
链表与列表的差异
Python中的list(列表)并不是传统意义上的列表,传统的意义上的列表就是我们今天讲的链表,链表真的是跟列表长的很像的啊,不同地方在于链表在数据存储方面更加的自由,其带有指示下一个结点未知的指向,也就是我可以随意的存储数据,只要我们有东西能找到我,我也可以找到我的下一个。而列表则只能分配在一段连续的存储空间里,且是作为一个整体,这就大大限制了数据的变更操作,但其在进行一些基础简单的操作时,时间复杂度极低。
list(列表):动态的顺序表
链表:存储地址分散的,需要“指针”指向的线性表
为什么要学习链表
知乎中的一个高赞回答:
我觉得家里的承重墙没实际意义,正忙着拆它呢。有事等我拆完了再说。
拆完了。
链表插入删除效率极高,达到O(1)。对于不需要搜索但变动频繁且无法预知数量上限的数据,比如内存池、操作系统的进程管理、网络通信协议栈的trunk管理等等等等,缺了它是绝对玩不转的。
甚至就连很多脚本语言都有内置的链表、字典等基础数据结构支持。哪怕只是稍微像点样子的小项目,如果缺了链表……谁扔的小石子?……啊,缺了链表绝对玩不转。保守估计,缺了链表,普通家用PC至少得慢10倍,网络服务器慢数百到数万倍都有可……啊,房子要塌
Python实现链表这一数据结构能简化我们的阅读,他没有C语言里面一大堆指针和内存分配那样的晦涩难懂,站在了一种更高层次的地方来方便快捷的实现链表这一数据结构,易于大家的理解。
后记
害!我也不知道我讲清楚了没得,列表和链表这个玩意儿确实是傻傻分不清,怪就怪在Python封装了一个这么好的列表,让链表这个兄弟在大多数的时候都没地方混,列表是真的好用,但是我们为了学习数据结构也应该要掌握一下链表。
大家可以参考一下其他大佬写的文章:
python学习笔记 – list内部实现(转)https://www.jianshu.com/p/cd75475168a
Python的列表(List)的底层实现原理 https://blog.csdn.net/Yuyh131/article/details/83592608
毕竟是新手,写的不好或者不对的地方欢迎指正,一起加油!
新手上路,技术有限,不喜勿喷!