链表结构
链表结构包含了链接到其他项的项
最简单:单链表结构和双链表结构
单链表结构 | 通过沿着一个外部的头链接来访问第1个项,通过从第一项产生串联起来的单个的链条访问其他项 | 很容易获取后继项 |
双链表结构 | 两个方向的链接 | 后继项前继项都容易访问 |
这两种结构的最后一项都没有指向下一项的链接(缺少链接=空链接)
一个双链表第一项是没有指向其前驱项的链接的
无法通过指定索引位置来立即访问一项,需从结构的一段开始进行直到想要的项
1 一旦找到一个插入或删除节点可直接进行删除和插入,不需要在内存中移动数据项
2 每一次删除插入,会调整大小而不需额外内存
数组项必须存储在连续的内存中 | 数组中项的逻辑顺序和内存中的物理单元紧密耦合 |
链表-非连续性内存 | 链表结构给定一个项的位置和地址,就可以在内存中找到它的单元 |
链表结构中的基本单位是节点
单链表的节点包含:一个数据项+到下一个节点的一个链接
双链表增加了到结构前一个节点的链接
单链接节点
节点变量会初始化为None值或一个新的Node对象
由上,通常在尝试访问一个给定节点变量的字段之前,可以询问其是否为None
链表也是通过循环来处理的
通过循环创建一个链表结构,并访问每一个节点
最近插入的项总是位于结构的开始处(显示数据和插入数据顺序相反)
1 遍历操作
遍历会访问每一个节点,遇到空链接时终止(线性+无需额外内存)
2 搜索O(n)
空链接/目标数据
Probe = head
While probe != None and targetItem != probe.data:
Probe = probe.next
If probe != None:
<targetItem has been found>
Else:
<targetItem is not in the linked structure>
3 替换O(n)
以下操作链表结构比数组结构更合适
4 在开始处插入O(1)
Head = Node(newItem, head)
在一个链表的开始处插入数据,时间和内存都是常数
5 在末尾插入
- head指针为None,将head指针设置为新节点
- head指针不为None,搜索最后一个节点,将next指向新的节点
在时间上是线性的,在空间上是常数的
6 从开始处删除O(1)
时间和内存都是常数的
7 从末尾删除
- 只有一个节点,head指针设置为None
- 在最后一个节点之前没有节点,搜索倒数第二个节点并将其next指针设置为None
时间和内存都是线性的
8 在任意位置插入O(n)
在i位置插入,先找到i-1(i<n)或n-1(i>=n)的节点
- 该节点的next指针为None-----> i>=n,新项放到结尾
- next不为None-----> i<n,新项放入i-1和i节点之间
插入操作必须计数节点。目标索引可能>=节点数目(小心避免超出链表结尾),所以:
循环额外条件:测试当前节点的next指针,看是否为最后一个节点
性能线性,内存常数
9 在任意位置删除O(n)
删除第i个项:
i<=0 | 删除第一项代码 |
0<i<n | 搜索i-1位置节点,删除其后面的节点 |
i>n | 删除最后一个节点 |
注意头节点的实时更新
复杂度权衡:时间、空间和单链表结构
单链表结构相对于数组的主要优点不是时间性能而是内存性能
1 调整链表结构大小,时间内存常数
2 链表结构物理大小不会超过逻辑大小,不会浪费内存
但是:单链表结构必须为指针使用n个内存单元格,双链表中加倍
链表的变体
带有额外的指针,提升性能并简化代码
A 带有一个哑头节点的循环链表结构
循环链表结构包含了从结构中最后一个节点返回到第一个节点的一个链接
哑头节点:不包含数据,但是充当了链表结构开头和结尾的一个标记
在空的链表结构中,head指针指向了哑头节点,且哑头节点的next指针指回到了其自身
空链表最初结构:
只需要考虑 第i个节点位于当前的第i个节点和它前一个节点之间的 情况
B 双链表结构
1 从给定节点向左移动前一个节点 next, previous
2 直接移动到最后一个节点 tail(external)
在结尾插入新的项
1 新节点的previous必须指向当前的尾节点
2 当前尾节点的next必须指向新节点
3 tail必须指向新节点