1-定义
线性表:零个或多个数据元素构成的有限序列,可以用序偶关系表示。
线性表的物理实现方式:数组与指针链。以数组方式实现时,称顺序表;以指针链实现时,称链表。
2-顺序表的C语言形式
需要说明的是:通常将一片连续的存储空间,称为内存数组;以内存数组来实现的线性表,叫顺序表;在许多编程语言中,会将顺序表称为数组。
操作系统分配内存时,需要用户指定待分配内存的大小,而数组申请内存空间时,需要指定两个信息:单个数据元素的内存大小,数组内的数据元素个数。数据元素在C语言中以固定大小的结构体表示。
// 数据元素
typeof struct { /* key-value pairs */} ElemType;
顺序表的表示:
#define DEFAULT_SIZE
typeof struct {
ElemType elements[DEFUALT_SIZE] // 储存数据元素的内存数组
int length; // 已存储的数据元素个数
int capacity; // 可容纳的数据元素个数
}
// 或者
typeof struct {
ElemType *elem // 数组的基地址
int length; // 已存储的数据元素个数
int capacity; // 可容纳的数据元素个数
}
3-链表的C语言形式
链表:每个节点内既存储了数据元素,又存储了后继节点的地址,节点之间地址不要求连续;
typeof struct Node {
ElemType data; // 数据域,存储数据元素
struct Node *next // 指针域,存储后继节点地址
struct Node *prev // 指针域,存储前驱节点地址, 可选
}
4-链表的JS实现
Javascript程序的内存是由运行时管理的,也没有指针数据类型,但JS对象是引用型,可以模拟指针操作。
数据结构的基本操作:查询、新增、删除,求链表的长度、遍历等;
操作基本可以分为两类:访问型与加工型操作。
一般,进行访问型和加工型操作时,需要考虑两个边界条件:
- 数据结构为空;
- 只有一个数据元素;
对于加工型的操作,还需考虑加工位置:
- 头部新增;
- 尾部新增;
- 头部与尾部之间的某个位置进行新增;
在书写程序的过程中,我们理想的程序追求最短的时间复杂度与最小的内存分配,但在实践中,较少的时间复杂度需要分配更多的内存,而较小的内存分配需要更长的时间复杂度,两者往往不能兼得,需要根据实际情况权衡取舍。
在链表的操作中,求链表长度与尾部新增是很常见的操作,但是对于单链表,由于只保存了头节点的引用,使得求链表长度与尾部新增都需要遍历整个链表,时间复杂度为O(n),但如果在单链表中增加一个length属性来保存节点数,一个尾指针来保存最后一个节点的引用,那么使得这两种操作的时间复杂度降低为O(1)。虽然内存新增了一点点,但对于性能有极大的提升。当然,新增一个尾指针也会增加程序的复杂性,因为在进行新增或删除时,可能需要一直更新尾指针
class Node {
constructor(data) {
this.data = data
this.next = null
}
// 可根据实际需要,自定义
compare(data) { return Object.is(this.data, data) }}
class LinkList {
constructor() {
this.head = null
this.tail = null
this.length = 0
}
// 头部新增
appendAtHead(data) {
const node = new Node(data)
if(!this.tail) this.tail = node
node.next = this.head
this.head = node
this.length++
}
// 尾部新增
appendAtTail(data) {
const node = new Node(data)
if(!this.head) {
this.head = this.tail = node
this.length++
return
}
this.tail.next = node
this.tail = node
this.length++ }
// 头部删除
removeAtHead() {
if(!this.head) return null
const data = this.head.data
if(this.head === this.tail) {
this.head = this.tail = null
} else {
this.head = this.head.next
}
this.length--
return data
}
// 尾部删除
removeAtTail() {
if(!this.head) return null
if(this.head === this.tail) {
return this.removeAtHead()
}
const data = this.tail.data
let prev = null, current = this.head
while(current !== this.tail) {
prev = current
current = current.next
}
prev.next = null
this.tail = prev
this.length--
return data
}
remove(data) {
let prev = null, current = this.head
while(current) {
if(current.compare(data)) {
if(current === this.head) return this.removeAtHead()
prev.next = current.next
this.length--
return current.data
}
prev = current
current = current.next
}
return null
}
contain(data) {
let current = this.head
while(current) {
if(current.compare(data)) return true
current = current.next
}
return false
}
find(data) {
let current = this.head
while(current) {
if(current.compare(data)) return current
current = current.next
}
return null
}
// 使用JS语言特性,使可迭代
[Symbol.iterator]() {
let current = this.head
return {
next() {
let value, done
if(current) {
value = current.data;
done = !!current.next
current = current.next
} else {
value = null
done = false
}
return { value, done }
},
[Symbol.iterator]() { return this }
}
}
}
单链表中,头部新增、头部删除、尾部新增的时间复杂度为O(1),而尾部删除的时间复杂度为O(n);头部新增与头部删除,可以实现链栈;尾部新增与头部删除可以实现链队列;
数组中,头部新增与删除的时间复杂度为O(n),尾部新增与删除的时间复杂度为O(1);尾部增删可以实现顺序栈;
单链表中其他位置的增删,需要O(n)的查询,O(1)的增/删;数组中其他位置的增删,需要O(1)的查询,O(n)的移动;