数据结构笔记--线性表

写在前面的话

做了几年的开发,越来越感觉到计算机基础知识的重要性,虽然在开发中可能直接使用计算机基础知识的地方不是太明显。当深入了解某个框架或者某个知识的原理时,需要一些基础知识,然而随着时间的推移基础知识长时间没看而逐渐遗忘或者不清晰,那时感觉力不从心,因此决定,复习一下基础知识做个总结

线性表

  线性表是最常用且最简单的一种数据结构,简言之,一个线性表是 n 个数据元素的有序序列.

特点

  1. 只有一个首结点和尾结点;
  2. 除首尾结点外,其他结点只有一个直接前驱和一个直接后继。

  简言之,线性结构反映结点间的逻辑关系是 一对一的,
线性结构包括线性表、堆栈、队列、字符串、数组等等,其中,最典型、最常用的

线性表的划分

 从存储结构上来划分,可分为顺序存储结构称顺序表和链式存储结构称链表

顺序表

顺序表的存储定义

 把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。简单来说,逻辑上相邻,物理上也相邻

顺序存储结构

顺序表的特点:

  1. 利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致
  2. 在访问线性表时,可以快速地计算出任何一个数据元素的存储地址。因此可以粗略地认为,访问每个元素所花时间相等 

顺序表的复杂度:

时间复杂度: 顺序表的查找、插入、删除算法的平均时间复杂度为O(n)

空间复杂度: 顺序表的时间复杂度为O(1)

顺序表的优缺点:

优点:
存储密度大(结点本身所占存储量/结点结构所占存储量)
可以随机存取表中任一元素

缺点:
在插入、删除某一元素时,需要移动大量元素
浪费存储空间
属于静态存储形式,数据元素的个数不能自由扩充

链表

链表的特点

结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻线性表的链式表示又称为非顺序映像或链式映像。

结点

  数据元素的存储映像。由数据域和指针域两部分组成.
数据域:存储元素数值数据.指针域:存储直接后继结点的存储位置
数据域 | 指针域
—|—

注意: ==头指针,头结点,首元结点==这几个概念:

  • 头指针: 是指向链表中第一个结点的指针
  • 首元结点:是指链表中存储第一个数据元素a1的结点
  • 头结点:是在链表的首元结点之前附设的一个结点;数据域内只放空表标志和表长等信息。其作用主要是为了链表的操作方便

头结点可以有也可以没有,但是头指针必须要有。

链表的形式:

单链表

结点只有一个指针域的链表。每个结点中除了包含数据域以外还包含一个指针域,用于指向其后继结点。

数据域 指针域

单链表的存储映像

  • 带头结点的单链表:头指针指向其头结点,头结点的值域可以不含任何信息,从头结点的后继结点开始存储信息
  • 不带头结点的单链表:头指针指向其首元结点

单链表的实现

单链表是否为空
1
2
3
func isEmpty() -> Bool {
return head == nil
}
获取首元结点
1
2
3
public var first:Node?{
return head
}
获取尾结点
1
2
3
4
5
6
7
8
9
10
public var last:Node? {
if var node = head {
while case let next? = node.next {
node = next
}
return node
} else {
return nil
}
}
链表的长度
1
2
3
4
5
6
7
8
9
10
11
12
public var count:Int {
if var node = head {
var c = 1
while case let next? = node.next {
node = next
c += 1
}
return c
} else {
return 0
}
}
结点的获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func getNode(atIndex index:Int) -> Node? {
if index >= 0 {
var node = head
var i = index
while node != nil {
if i == 0 {
return node
}
i -= 1
node = node?.next
}
}
return nil
}
链表的添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public func append(_ value: T) {
let newNode = SLNode(value: value)
if let lastNode = last {
lastNode.next = newNode
} else {
head = newNode
}
}
// 批量添加
public func append(_ values:Array<T>) {
for value in values {
append(value)
}
}
链表的插入 插入在第 index 结点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public func insertNode(atIndex index:Int,value:T) {
let oldNode = getBeforeNode(atIndex: index)
var newNode = SLNode(value: value)
if index > 0 {
if let old = oldNode {
newNode.next = old.next
old.next = newNode
} else {
append(value)
}
} else {
append(value)
}
}
单链表删除

删除示意图

1
2
3
4
5
6
7
8
9
10
11
12
    删除在某一个 位置的结点
public func removeNode(atIndex index:Int) {
let beforeNode = getBeforeNode(atIndex: index)
if let before = beforeNode {
before.next = before.next?.next
}
}

// 删除所有结点
public func removeAll() {
head = nil
}

双链表

 有两个指针域的链表。双链表就是在单链表上增加一个指针域,指向当前结点的前驱.用于方便地找到其前驱结点.和单链表类似也分为带头结点的双链表和不带头结点的双链表.

指针域 数据域 指针域

双向链表的实现

定义一个双向链表的结点

1
2
3
4
5
6
7
8
9
//定义一个 结点 双向链表的结点定义格式
public class DLNode<T> {
var value: T
var next :DLNode?// 下一个结点
weak var previous :DLNode?//前一个结点
public init(value:T) {
self.value = value
}
}

定义一个双向链表

1
2
3
4
5
6
7
public final class DoubleLinkList<T> {
//为了操作方便 将DLNode<T> 重新命名为 Node
public typealias Node = DLNode<T>
//头指针
fileprivate var head:Node?
public init() {}
}

双向链表是否为空

1
2
3
public var isEmpty:Bool {
return head == nil //首元结点是否为空
}

双向链表获取首元结点

1
2
3
public var first:Node?{
return head
}

双向链表获取尾结点

  尾结点的next 指针指向的结点必定为空。所以从首结点开始遍历,如果 next 为空比为尾结点

1
2
3
4
5
6
7
8
9
10
public var last:Node? {
if var node = head {
while case let next? = node.next {
node = next
}
return node
} else {
return nil
}
}

双向链表的长度

 从首元结点开始遍历,一直遍历到尾结点,直到遍历完成,每有一个结点长度加一

1
2
3
4
5
6
7
8
9
10
11
12
public var count:Int {
if var node = head {
var c = 1
while case let next? = node.next {
node = next
c += 1
}
return c
} else {
return 0
}
}

双向链表的查找:

  从首元结点开始相后查找 复杂度为 O(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public func node(atIndex index: Int) -> Node? {
if index >= 0 {
var node = head
var i = index
while node != nil {//从首元结点开始向后查找
if i == 0 {
return node
}
i -= 1
node = node!.next
}
}
return nil
}

双向链表的插入

 在某个位置插入一个结点。双向链表的插入步骤;


1
2
3
4
5
6
7
8
9
10
11
public func insert(_ node: Node, atIndex index: Int) {
let oldNode = getNode(atIndex: index)
let newNode = DLNode(value: node.value)
newNode.previous = oldNode?.previous
oldNode?.previous?.next = newNode
newNode.next = oldNode
oldNode?.previous = newNode
if oldNode?.previous == nil {
head = newNode
}
}

双向链表的删除

 删除链表中某一个结点,步骤如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@discardableResult public func remove(atIndex index: Int) -> T {
let node = self.getNode(atIndex: index)
assert(node != nil)
return remove(node: node!)
}





@discardableResult public func remove(node: Node) -> T {
let prev = node.previous
let next = node.next

if let prev = prev {
prev.next = next
} else {
head = next
}
next?.previous = prev

node.previous = nil
node.next = nil
return node.value
}

双向链表翻转

1
2
3
4
5
6
7
8
public func reverse() {
var node = head
while let currentNode = node {
node = currentNode.next // 把下一个赋值给node
swap(&currentNode.next, &currentNode.previous) // 交换
head = currentNode
}
}
  • 以上为双向链表的操作,单链表和单循环链表以及双向循环链表的操作实现可以参照双向链表的操作。

链表的运算效率分析

  1. 查找: 因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)。

  2. 插入和删除: 因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。

但是,如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为 O(n) 。

循环单链表

 单链表的最后一个指针域指向链表中的第一个结点即可。循环单链表可以以实现从任何一个结点出发访问链表中的任何结点。

  • 注意: 这里说第一个结点而不是说首元结点是因为,如果循环单链表是带头结点的,则最后一个结点的指针域要指向头结点,如果循环单链表的不带头节点,则最后一个指针域要指向首元结点

循环双链表

 首尾相接的链表,源自于双链表。即将终端结点的后继结点设为第一个结点(首元结点或者头结点)将链表中的第一个结点的前继结点设为终端结点。

链表的优缺点

  • 优点
  1. 数据元素的个数可以自由扩充
  2. 插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高
  • 缺点:
  1. 存储密度小
  2. 存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问(顺藤摸瓜)

顺序表和链表的比较:

基于空间的比较

  1. 存储分配方式:顺序表的存储空间是静态分配的,链表的存储空间是动态分配的
  2. 存储密度:(存储密度= 结点值域所占的存储量/结点结构所占的存储总量),顺序表的存储密度 = 1 链表的存储密度 < 1 (因为结点中存储的有指针域)

基于时间的比较:

  1. 存取方式:顺序表可以随机存取,也可以顺序存取,链表是顺序存取
  2. 插入删除时移动的元素的个数:顺序表平均需要移动近一半的元素,链表不需要移动,只需要修改指针

更多详细代码

坚持原创,您的支持将鼓励我继续创作!