数据结构笔记系列 数据结构笔记—两个有序链表合并成一个有序链表
线性表
线性表是最常用且最简单的一种数据结构,简言之,一个线性表是 n 个数据元素的有序序列.
特点
- 只有一个首结点和尾结点;
- 除首尾结点外,其他结点只有一个直接前驱和一个直接后继。
简言之,线性结构反映结点间的逻辑关系是 一对一的, 线性结构包括线性表、堆栈、队列、字符串、数组等等,其中,最典型、最常用的
线性表的划分
从存储结构上来划分,可分为顺序存储结构称顺序表和链式存储结构称链表
顺序表
顺序表的存储定义
把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。简单来说,逻辑上相邻,物理上也相邻
顺序表的特点:
- 利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致
- 在访问线性表时,可以快速地计算出任何一个数据元素的存储地址。因此可以粗略地认为,访问每个元素所花时间相等
顺序表的复杂度:
时间复杂度: 顺序表的查找、插入、删除算法的平均时间复杂度为O(n)
空间复杂度: 顺序表的时间复杂度为O(1)
顺序表的优缺点:
优点: 存储密度大(结点本身所占存储量/结点结构所占存储量) 可以随机存取表中任一元素
缺点: 在插入、删除某一元素时,需要移动大量元素 浪费存储空间 属于静态存储形式,数据元素的个数不能自由扩充
链表
链表的特点
结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻线性表的链式表示又称为非顺序映像或链式映像。
结点
数据元素的存储映像。由数据域和指针域两部分组成. 数据域:存储元素数值数据.指针域:存储直接后继结点的存储位置
注意: ==头指针,头结点,首元结点==这几个概念:
- 头指针: 是指向链表中第一个结点的指针
- 首元结点:是指链表中存储第一个数据元素a1的结点
- 头结点:是在链表的首元结点之前附设的一个结点;数据域内只放空表标志和表长等信息。其作用主要是为了链表的操作方便
头结点可以有也可以没有,但是头指针必须要有。
链表的形式:
单链表
结点只有一个指针域的链表。每个结点中除了包含数据域以外还包含一个指针域,用于指向其后继结点。
单链表的存储映像
- 带头结点的单链表:头指针指向其头结点,头结点的值域可以不含任何信息,从头结点的后继结点开始存储信息
- 不带头结点的单链表:头指针指向其首元结点
单链表的实现
单链表是否为空
func isEmpty() -> Bool {
return head == nil
}
复制代码
获取首元结点
public var first:Node?{
return head
}
复制代码
获取尾结点
public var last:Node? {
if var node = head {
while case let next? = node.next {
node = next
}
return node
} else {
return nil
}
}
复制代码
链表的长度
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
}
}
复制代码
结点的获取
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
}
复制代码
链表的添加
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 结点
public func insertNode(atIndex index:Int,value:T) {
let oldNode = getBeforeNode(atIndex: index)
let newNode = SLNode(value: value)
if index > 0 {
if let old = oldNode {
newNode.next = old.next
old.next = newNode
} else {
append(value)
}
} else if (index == 0 ) {
newNode.next = head
head = newNode
} else {
append(value)
}
}
复制代码
单链表删除
删除在某一个 位置的结点
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
}
复制代码
双链表
有两个指针域的链表。双链表就是在单链表上增加一个指针域,指向当前结点的前驱.用于方便地找到其前驱结点.和单链表类似也分为带头结点的双链表和不带头结点的双链表.
双向链表的实现
定义一个双向链表的结点
//定义一个 结点 双向链表的结点定义格式
public class DLNode<T> {
var value: T
var next :DLNode?// 下一个结点
weak var previous :DLNode?//前一个结点
public init(value:T) {
self.value = value
}
}
复制代码
定义一个双向链表
public final class DoubleLinkList<T> {
//为了操作方便 将DLNode<T> 重新命名为 Node
public typealias Node = DLNode<T>
//头指针
fileprivate var head:Node?
public init() {}
}
复制代码
双向链表是否为空
public var isEmpty:Bool {
return head == nil //首元结点是否为空
}
复制代码
双向链表获取首元结点
public var first:Node?{
return head
}
复制代码
双向链表获取尾结点
尾结点的next 指针指向的结点必定为空。所以从首结点开始遍历,如果 next 为空比为尾结点
public var last:Node? {
if var node = head {
while case let next? = node.next {
node = next
}
return node
} else {
return nil
}
}
复制代码
双向链表的长度
从首元结点开始遍历,一直遍历到尾结点,直到遍历完成,每有一个结点长度加一
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)
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
}
复制代码
双向链表的插入
在某个位置插入一个结点。双向链表的插入步骤;
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
}
}
复制代码
双向链表的删除
删除链表中某一个结点,步骤如下
@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
}
复制代码
双向链表翻转
public func reverse() {
var node = head
while let currentNode = node {
node = currentNode.next // 把下一个赋值给node
swap(¤tNode.next, ¤tNode.previous) // 交换
head = currentNode
}
}
复制代码
- [x] 以上为双向链表的操作,单链表和单循环链表以及双向循环链表的操作实现可以参照双向链表的操作。
链表的运算效率分析
-
查找: 因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)。
-
插入和删除: 因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。
但是,如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为 O(n) 。
循环单链表
单链表的最后一个指针域指向链表中的第一个结点即可。循环单链表可以以实现从任何一个结点出发访问链表中的任何结点。
- [x] 注意: 这里说第一个结点而不是说首元结点是因为,如果循环单链表是带头结点的,则最后一个结点的指针域要指向头结点,如果循环单链表的不带头节点,则最后一个指针域要指向首元结点
循环双链表
首尾相接的链表,源自于双链表。即将终端结点的后继结点设为第一个结点(首元结点或者头结点)将链表中的第一个结点的前继结点设为终端结点。
链表的优缺点
- 优点
- 数据元素的个数可以自由扩充
- 插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高
- 缺点:
- 存储密度小
- 存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问(顺藤摸瓜)
顺序表和链表的比较:
基于空间的比较
- 存储分配方式:顺序表的存储空间是静态分配的,链表的存储空间是动态分配的
- 存储密度:(存储密度= 结点值域所占的存储量/结点结构所占的存储总量),顺序表的存储密度 = 1 链表的存储密度 < 1 (因为结点中存储的有指针域)
基于时间的比较:
- 存取方式:顺序表可以随机存取,也可以顺序存取,链表是顺序存取
- 插入删除时移动的元素的个数:顺序表平均需要移动近一半的元素,链表不需要移动,只需要修改指针