这系列博客面向有python基础,想要了解链表、二叉树、图的python实现方式和一些操作技巧以及在积极刷题准备面试的同学,在博客里会有部分比较经典的关于链表、二叉树和图的面试题及python的解题方式。在概念阐述上会部分参考严蔚敏老师的数据结构(C语言版)一书,题目选取上会参考python程序员面试算法宝典。好了,闲话不多说,Python中的数据结构,Let's begin!
线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素。假设线性表lst的每一个元素需要占用a个存储单元,以第一个单元的存储地址作为该元素的存储位置,那么线性表中第i+1个元素的存储位置Loc(lsti+1)和第i个元素的存储位置Loc(lst[i])满足以下关系:(在这里就偷懒用lst[i+1]表示lst的第i+1个元素)
Loc(lst[i+1]) = Loc(lst[i]) + a
那么线性表的第i个数据元素lst[i]的存储位置为:
Loc(lst[i]) = Loc(lst[1]) + (i - 1) × a(这里说明一下lst[1]是指该lst中的第一个元素,在真实python环境下lst的第一个元素应该是lst[0],这里为了便于理解所以这样阐述)
那么在python中呢,list与tuple都能够实现线性表的顺序表示,两者的区别就是tuple不可变而list可变,在入门python时大家都听说过一句话,tuple是不可变的list(当然tuple不会这个弱鸡,大家对如何使用tuple感兴趣的话可以参考Fluent Python,应该就在这本书的前几章,在这里由于不是重点就不谈了~)。
下面来通过代码看一下如何在python中定义和使用list和tuple,以及它们中元素的存储地址。
lst = [1, 2, 3, 4, 5] # 定义一个list
tup = (6, 7, 8, 9, 10)
for i in range(len(lst)):
print('lst中第{}个元素是{},它的内存地址为{}'.format(i + 1, lst[i], id(lst[i]))) # 依次输出list中的元素及他们的内存地址
print('\n')
for i in range(len(tup)):
print('tup中第{}个元素是{},它的内存地址为{}'.format(i + 1, tup[i], id(tup[i]))) # 依次输出list中的元素及他们的内存地址
运行结果:
大家可以看到不管是list中还是tuple中元素存储的内存地址都是连续的,由于存储的都是int类型,因此每个元素在内存中占32位即4个字节。因此在顺序表示的线性表中,线性表的任一数据元素都可随机存取。list的插入和删除操作可通过list()类内置的insert()函数和remove()函数轻松完成,这里就不多谈了。
顺序表示的线性表可以随机存取表中任一元素,十分方便,但是这个特点也铸成了这种存储结构的弱点:在作插入或删除操作时,需移动大量元素。为了克服这个缺点,线性表的链式存储结构诞生了,这也是线性表章节的重点。
线性表的链式存储结构(下称链表)的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以连续也可以不连续,当然用链式存储一般都是不连续的),因此,为了表示每个数据lst[i]与其直接后继数据元素lst[i+1]之间的逻辑关系,对数据元素lst[i]来说,除了存储其本身的信息之外,还需要存储一个指示其直接后继的信息(即直接后继的存储位置)。
下面简单介绍一下链表中比较重要的一个概念:头结点。在单链表的第一个元素之前附设一个类型相同的结点,称它为头结点,在头结点中可以不存储任何信息也可以存储该链表的长度等信息。这里需要说明的是在python中是没有指针的概念的,类似于指针的功能我们会使用引用来实现。在阐述时依旧会使用指针。有头结点和无头结点的链表如下图所示(忽略我几年不手写的丑字)。
头结点具有如下功能(简要说明):对于有头结点的指针,我们在插入或者删除链表的任何结点时,所要做的都是修改前一个结点的指针域,因为所有的含有元素的结点都有前驱结点。如果是无头结点的链表,那么它的首元素是没有前驱结点的,需要分开讨论。
在Python中,我们通常会定义一个Node类来使用链表,如下所示:
class Node: # 结点类
def __init__(self, val):
self.val = val # 结点的值
self.next = None # 指向当前结点的后继结点
node_1 = Node(None) # 头结点
node_2 = Node(1) # 第一个元素结点
node_3 = Node(2)
node_4 = Node(3)
node_1.next = node_2
node_2.next = node_3
node_3.next = node_4
node = node_1 # 指向头结点
count = 1
while node.next:
node = node.next
print('链表中第{}个元素是{}'.format(count, node.val))
运行结果:
上面我演示了python中链表的遍历方式。话不多说,相信部分数据结构基础好的同学都嫌我啰嗦了,下面列了几道经典的数据结构面试题。
这是链表考的非常常规的一道题,也比较简单,有多种解决方案,我在这里介绍一种时间复杂度为O(n)的解。这种方法非常简单,主要思路就一句话:遍历链表,将遍历到的结点插入到头结点的后面。一脸懵逼是不是?直接上代码:
def list_reverse(head_node):
if not head_node or not head_node.next: # 判断列表是否为空
return
cur_node = head_node.next.next # 指向链表的第二个元素(不包括头结点)
head_node.next.next = None # 将链表的第一个元素的next置None,因为该元素为逆序后的最后一个元素
while cur_node:
next_node = cur_node.next # 保存当前结点的下一个结点
cur_node.next = head_node.next # 当前结点的下一个结点置为头结点的第一个结点
head_node.next = cur_node # 头结点的下一个节点置为当前结点
cur_node = next_node
return head_node
测试程序:
if __name__ == '__main__':
head = Node(None) # 头结点
node_2 = Node(1) # 第一个元素结点
node_3 = Node(2)
node_4 = Node(3)
node_5 = Node(4)
head.next = node_2
node_2.next = node_3
node_3.next = node_4
node_4.next = node_5
count = 1
node = head
while node.next:
node = node.next
print(node.val, end=' ')
print("\n逆序后:")
node = list_reverse(head)
while node.next:
node = node.next
print(node.val, end=' ')
结果:
算法的主要思路注释上面有,整体思路非常简单,时间复杂度和空间复杂度也都尽如人意。
快慢指针是链表题中非常非常重要的方法。
非常常见的一道题,比较容易想的方法是先遍历一遍链表得到链表的长度n,之后求倒数第k个元素就可以转化为求顺序的第n-k个元素,但是需要对链表进行二次遍历。
使用快慢指针只需对链表遍历一次,具体思路为:设置快慢指针fast和slow,fast先行k步,之后fast和slow一起遍历,当fast遍历到链表尾部(None)时,slow所指结点值即为该链表的倒数第k个元素。
def last_k(head_node, k):
if not head_node or not head_node.next: # 判断列表是否为空
return
fast = head_node.next # 快指针
slow = head_node.next # 慢指针
for i in range(k): # 快指针先行k步
if fast.next:
fast = fast.next
else: # k大于链表长度,return
return
while fast: # 当fast不为None时,快慢指针同时遍历
fast = fast.next
slow = slow.next
return slow.val
测试程序:
if __name__ == '__main__':
head = Node(None) # 头结点
node_2 = Node(1) # 第一个元素结点
node_3 = Node(2)
node_4 = Node(3)
node_5 = Node(4)
head.next = node_2
node_2.next = node_3
node_3.next = node_4
node_4.next = node_5
print("倒数第二个元素为{}".format(last_k(head, 2)))
结果:
未完待续。。。