线性表--链表基础
Posted Abro.
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线性表--链表基础相关的知识,希望对你有一定的参考价值。
目录
前言
线性表的顺序存储又称顺序表,是用一组地址连续的存储单元依次存储表中的数据元素,使逻辑上相邻的两个元素在物理位置上也相邻。
线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素,对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。
一、链表简介
1.1 链表的定义
链表(Linked List):是一种线性表数据结构。它使用一组任意的存储单元(可以是连续的,也可以是不连续的),来存储一组具有相同类型的数据。
单链表:
链表结构的优缺点:
- 优点:存储空间不必事先分配,在需要存储空间的时候可以临时申请,不会造成空间的浪费;一些操作的时间效率远比数组要高(比如:插入、移动、删除元素等)。
- 缺点:不仅数据元素本身的数据信息要占用存储空间,指针也需要占用存储空间,链表结构比数组结构的空间开销要大。
1.2 双向链表
双向链表(Doubly Linked List):链表的一种,也叫双链表。它的每个链节点中有两个指针,分别指向直接前驱和直接后继。
从双链表的任意一个节点开始,都可以很方便的访问它的前驱节点和后继节点。
1.3 循环链表
循环链表(Circular linked list):链表的一种。它的最后一个链节点指向头节点,形成一个环。
从循环链表的任何一个节点出发都能找到任何其他节点。
1.4 静态链表
基本结构(概念):静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,其指针是结点的相对地址(数组下标),又称游标。
特点:
- 静态链表以next==-1作为其结束的标志
- 静态链表的插入,删除操作与动态链表的相同,只需要修改指针,而不需要移动元素
- 静态链表要预先分配一块连续的内存空间
优缺点:
- 优点:增、删操作不需要大量移动元素
- 缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变
适用场景:
- 不支持指针的低级语言;
- 数据元素数量固定不变的场景(如操作系统的文件分配表FAT)
二、链表的基本操作
2.1 链表的结构定义
链表是由链节点通过 next 链接而构成的,所以先来定义一个简单的链节点类,即 ListNode类。ListNode类使用成员变量 val 表示数据元素的值,使用指针变量 next 表示后继指针。
然后再定义链表类,即 LinkedList
类。ListkedList
类中只有一个链节点变量 head
用来表示链表的头节点。
我们在创建空链表时,只需要把相应的链表头节点变量设置为空链接即可。在 Python
里可以将其设置为 None
,其他语言也有类似的惯用值,比如 NULL
、nil
、0
等。
2.2 建立一个线性链表
建立一个线性链表的过程是:根据线性表的数据元素动态生成链节点,并依次将其连接到链表中。其做法如下:
- 从所给线性表的第
1
个数据元素开始依次获取表中的数据元素。 - 每获取一个数据元素,就为该数据元素生成一个新节点,将新节点插入到链表的尾部。
- 插入完毕之后返回第
1
个链节点的地址。
建立一个线性表的时间复杂度O(n),n为线性表长度。
建立线性表的代码如下:
# 根据 data 初始化一个新链表
def create(self, data):
self.head = ListNode(0)
cur = self.head
for i in range(len(data)):
node = ListNode(data[i])
cur.next = node
cur = cur.next
2.3 求线性表的长度
线性链表的长度被定义为链表中包含的链节点的个数。求线性链表的长度操作只需要使用一个可以顺着链表指针移动的指针变量 cur
和一个计数器 count
。具体做法如下:
- 让指针变量
cur
指向链表的第1
个链节点。 - 然后顺着链节点的
next
指针遍历链表,指针变量cur
每指向一个链节点,计数器就做一次计数。 - 等
cur
指向为空时结束遍历,此时计数器的数值就是链表的长度,将其返回即可。
求线性链表的长度操作的问题规模是链表的链节点数 n
,基本操作是 cur
指针的移动,操作的次数为 n
,因此算法的时间复杂度为 O(n)。
2.4 查找元素
在链表中查找值为 val
的位置:链表不能像数组那样进行随机访问,只能从头节点 head
开始,沿着链表一个一个节点逐一进行查找。如果查找成功,返回被查找节点的地址。否则返回 None
。
查找元素操作的问题规模是链表的长度 n
,而基本操作是指针 cur
的移动操作,所以查找元素算法的时间复杂度为 O(n)。
2.5 插入元素
链表插入元素操作可分为三种:
- 头插法:在链表第 1 个 链节点之前插入值为 val 的链节点
- 尾插法:在链表最后 1 个链节点之后插入值为 val 的链节点
- 中间插入法:在链表第 i 个链节点之前插入值为 val 的链节点
2.5.1 链表头部插入元素
算法实现步骤:
- 先创建一个值为
val
的链节点node
。 - 然后将
node
的next
指针指向链表的头节点head
。 - 再将链表的头节点
head
指向node
。
在链表头部插入链节点的时间复杂度为O(1).
2.5.2 链表尾部插入元素
算法实现步骤:
- 先创建一个值为
val
的链节点node
。 - 使用指针
cur
指向链表的头节点head
。 - 通过链节点的
next
指针移动cur
指针,从而遍历链表,直到cur.next == None
。 - 令
cur.next
指向将新的链节点node
。
链表尾部插入节点的时间复杂度为O(n).
2.5.3 链表中间插入元素
算法实现步骤为:
- 使用指针变量
cur
和一个计数器count
。令cur
指向链表的头节点,count
初始值赋值为0
。 - 沿着链节点的
next
指针遍历链表,指针变量cur
每指向一个链节点,计数器就做一次计数。 - 当
count == index - 1
时,说明遍历到了第index - 1
个链节点,此时停止遍历。 - 创建一个值为
val
的链节点node
。 - 将
node.next
指向cur.next
。 - 然后令
cur.next
指向node
。
2.6 改变元素
将链表中第i个元素值改为 val:首先要先遍历到第 i 个链节点,然后直接更改第 i 个链节点的元素值。具体做法如下:
- 使用指针变量
cur
和一个计数器count
。令cur
指向链表的头节点,count
初始值赋值为0
。 - 沿着链节点的
next
指针遍历链表,指针变量cur
每指向一个链节点,计数器就做一次计数。 - 当
count == index
时,说明遍历到了第index
个链节点,此时停止遍历。 - 直接更改
cur
的值val
。
因为将 cur 从链表头部移动到第i个链节点的操作平均复杂度是 O(n),所以该算法的时间复杂度是O(n)。
2.7 删除元素
分为三种情况:
- 链表头部删除元素:删除链表的第
1
个链节点。 - 链表尾部删除元素:删除链表末尾最后
1
个链节点。 - 链表中间删除元素:删除链表第
i
个链节点。
2.7.1 链表头部删除元素
步骤如下:
- 将
self.head
沿着next
指针向右移动一步即可。
删除链表头结点的时间复杂度为O(1) .
2.7.2 链表尾部删除元素
步骤如下:
- 先使用指针变量
cur
沿着next
指针移动到倒数第2
个链节点。 - 然后将此节点的
next
指针指向None
即可。
需要移动 n 次 ,因此删除链尾结点的时间复杂度为O(n)。
2.7.3 链表中间删除元素
步骤如下:
- 先使用指针变量
cur
移动到第i - 1
个位置的链节点。 - 然后将
cur
的next
指针,指向要第i
个元素的下一个节点即可。
总结
顺序表 | 链表 | |
存取(读写)方式 | 顺序表可以顺序存取,也可以随机存取 | 链表只能从表头顺序存取元素 |
逻辑结构与物理结构 | 逻辑上相邻的元素,对应的物理存储位置也相邻 | 逻辑上相邻的元素,物理存储位置不一定相邻,对应的 逻辑关系是通过指针链接来表示 |
查找、插入和删除操作 | 按值查找:有序时可用折半查找,时间复杂度为O(log2n),无序时为O(n);按序号查找:时间复杂度O(1);插入和删除平均移动半个表长的元素 | 按值查找:无序时时间复杂度为O(n);按序号查找:链表的平均时间复杂度为O(n);插入和删除操作只需修改相关结点的指针域 |
空间分配 | 静态分配:存储空间满则无法扩容会出现内存溢出;动态分配:对空间进行扩充的时候需要进行数据移动,效率低下 | 空间分配非常灵活 |
以上是关于线性表--链表基础的主要内容,如果未能解决你的问题,请参考以下文章