目录
什么是线性表(Linear List)?
首先特别注意:线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构,两者属于不同层次的概念,不要将其混淆。
线性表是具有相同数据类型的 n(n>=0)个数据元素的有限序列,其中 n 为表长,当 n=0 时线性表是一个空表。若用 L 命名线性表,其一般表示为:
L = (a1,a2,...,ai,ai+1,...,an)
式中,a1 是唯一的“第一个”数据元素,又称表头元素;an 是唯一的“最后一个”数据元素,又称表尾元素。
除第一个元素外,每个元素有且只有一个直接前驱。除最后一个元素外,每个元素有且只有一个直接后继。
线性表具有如下特点:
表中元素的个数有限
表中元素具有逻辑上的顺序性,表中元素有其先后次序
表中元素都是数据元素,每个元素都是单个元素
表中元素的数据类型都相同,每个元素占用相同大小的存储空间
表中元素具有抽象性,只讨论元素间的逻辑关系,不考虑元素表示的内容
再次注意:线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构,两者属于不同层次的概念,不要将其混淆。
线性表的存储结构:
顺序存储
线性表的顺序存储又称顺序表。是用一组地址连续的存储单元依次存储线性表中的数据元素,使得逻辑上相邻的两个元素在物理位置上也相邻。顺序表中元素的逻辑顺序与其物理顺序相同(这是顺序表的特点)。
注意:线性表中元素的次序(也叫位序)是从 1 开始的,数组中元素的下标是从 0 开始的
顺序表具有的特点如下所示:
- 随机访问,即通过首地址和元素序号可在时间 O(1) 内找到指定的元素
- 存储密度高,每个节点只存储数据元素
- 逻辑上相邻的两个元素在物理上也相邻,插入和删除数据时需要移动大量元素
链式存储
线性表的链式存储又称单链表,是指通过一组任意的存储单元来存储线性表中的数据元素。
单链表可以解决顺序表需要大量存储单元的缺点,但单链表附加指针域,也存在浪费存储空间的缺点。单链表的存储空间离散地分布在存储空间中,单链表是非随机存取地存储结构。在使用单链表查找数据元素时,不能直接找到某个特定地结点,需要从表头开始遍历,依次比较查找。
链表带头结点和不带头结点的区别?
头指针:通常使用“头指针”来标识一个链表,如单链表L,头指针为NULL的时表示一个空链表。链表非空时,头指针指向的是第一个结点的存储位置。
头结点:在单链表的第一个结点之前附加一个结点,称为头结点。头结点的Data域可以不设任何信息,也可以记录表长等相关信息。若链表是带有头结点的,则头指针指向头结点的存储位置。
[注意]无论是否有头结点,头指针始终指向链表的第一个结点。如果有头结点,头指针就指向头结点。
两者区别:
1、不带头结点的单链表对于第一个节点的操作与其他节点不一样,需要特殊处理,这增加了程序的复杂性和出现bug的机会,因此,通常在单链表的开始结点之前附设一个头结点,可以理解为头结点是为了方便增删等操作的。
2、带头结点与不带头结点初始化、插入、删除、输出操作都不一样,在遍历输出链表数据时,带头结点的判断条件是while(head->next!=NULL),而不带头结点是while(head!=NULL),虽然头指针可以在初始时设定,但是如1所述,对于特殊情况如只有一个节点会出现问题。
链表的基本操作(以单链表为例)
1. 创建
(1)头插法创建单链表
从一个空表开始,生成新结点,并将读取到的数据存储到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后。头插法操作示意图如下所示:
头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序是相反的。每个结点的插入时间为 O(1),设单链表长为 n,则总时间复杂度为 O(n)。
注意:使用头插法创建单链表可以实现链表的逆序问题!(比如待插入的节点顺序是5 - 2 - 0 - 1 使用头插法后顺序就变为1 - 0 - 2 - 5)
(2)尾插法创建单链表
- 该方法将新节点插入到当前链表的表尾中,为此必须增加一个尾指针 r,使其始终指向当前链表的尾结点。该方法生成的链表中的结点的次序和输入数据的顺序一致。尾插法创建单链表示意图如下图所示:
- 附设了一个指向表尾结点的指针,故时间复杂度和头插法的相同。
2. 销毁
3. 增删改查等操作
下面是创建单链表的基本代码实现(尾插法):
/**
* @description: 创建链表单节点类
* @param {*} val 节点值
* @return {*}
*/
function ListNode(val) {
this.val = val
this.next = null
}
/**
* @description: 创建链表类
* @param {*}
* @return {*}
*/
function LinkedList() {
this.length = 0
this.head = null
}
// 向链表中追加节点
LinkedList.prototype.append = function (val) {
//创建所需要追加的节点
let node = new ListNode(val)
// 判断节点是否为空
if (!this.head) {
this.head = node
}
// 这里调用前面的getElementLoc函数找到尾节点
let tail = this.getElementLoc(this.length - 1)
tail.next = node
this.length++
}
// 在链表的指定位置插入节点
LinkedList.prototype.insert = function (index, val) {
if (index < 0 || index > this.length) return false
let node = new ListNode(val)
if (index === 0) {
//这里的等号不好理解,就理解为:从等号左边的节点指向等号右边的节点
node.next = this.head
this.head = node
}else {
let preN = this.getElementLoc(index - 1)
node.next = preN.next
preN.next = node
}
this.length++
return true
}
// 删除链表中指定位置的元素,并返回这个元素的值
LinkedList.prototype.removeLoc = function (index) {
if(index < 0 || index > this.length) return false
let cur = this.head
if(index === 0) {
this.head = cur.next
}else {
let preN = this.getElementLoc(index - 1)
cur = preN.next
preN.next = cur.next
}
this.length--
return cur.val
}
// 删除链表中对应的元素
LinkedList.prototype.remove = function (val) {
let index = this.indexOf(val)
return this.removeLoc(index)
}
// 获取链表中给定元素的索引
LinkedList.prototype.indexOf = function (val) {
let cur = this.head
for(let i = 0; i<this.length; i++) {
if(cur.val === val) return i
cur = cur.next
}
//找不到返回-1
return -1
}
// 获取链表中某个节点
LinkedList.prototype.find = function (val) {
// 这里的逻辑和getElementLoc类似都是从头开始遍历节点
let cur = this.head
while (cur) {
if(cur.val === val) return cur
cur = cur.next
}
return null
}
// 获取链表中索引所对应的元素
LinkedList.prototype.getElementLoc = function (index) {
if (index < 0 || index >= this.length) return null
if(index === 0) return this.head
// 新建一个节点指向头节点
let cur = this.head
//链表查找指定位置元素的复杂度是O(n),因为查找必须从头开始
// 下面这种写法不好理解:其实就是遍历index个节点找到目标节点
// while (index--) {
// cur = cur.next
// }
let i = 1//记录第一个节点下标
// 遍历循环链表
while (cur && i<=index) { //当前节点不为空且节点下标小于等于index
cur = cur.next
i++
}
return cur
}
// 判断链表是否为空
LinkedList.prototype.isEmpty = function () {
return !this.length
}
// 获取链表的长度
LinkedList.prototype.size = function () {
return this.length
}
// 获取链表的头元素
LinkedList.prototype.getHead = function () {
return this.head
}
// 清空链表
LinkedList.prototype.clear = function () {
this.head = null
this.length = 0
}
// 序列化链表
LinkedList.prototype.join = function (string) {
// 序列化链表即使用指定格式输出链表,类似于数组中 join 方法,此举旨在便于我们测试
let cur = this.head
let str = ''
while (cur) {
str += cur.val
if (cur.next) str += string
cur = cur.next
}
return str
}
双链表
为了克服单链表只能从头开始遍历,引入了双链表。(有prior指针指向前驱结点)
双链表的插入
注意第一二步操作必须在第四步操作之前,否则p结点的后继结点的指针会丢掉,导致插入失败(s节点的后继结点会指向自己本身,导致p结点后面的数据都会丢失!)
双链表的删除
循环单链表
循环双链表
插入操作
删除操作
静态链表
其实栈,队列和串之类的数据结构也是一种操作受限特殊的线性表,只不过他们在操作上不同于线性表。