常用的数据结构有:栈、队列、链表和有根树。本篇博文向大家介绍了栈、队列和链表,关于有根树会在后面关于树的算法中介绍。
栈和队列都是动态集合,且在其上进行delete操作所移除的元素都是预先设定的。在栈中,被删除的是最近插入的元素:栈实现的是一种后进先出策略。类似地,在队列中,被删除的总是在集合中存在时间最长的那个元素:队列实现的是一种先进先出策略。
1 栈
1.1 栈简介
栈上的insert操作称为压入(PUSH),而无元素参数的delete操作称为弹出(POP)。这两个名称使人联想到现实中的栈。盘子从栈中弹出的次序刚好同它们压入的次序相反,这是因为只有最上面的盘子才能被取下来。
1.2 设计栈
1.2.1 栈YJStack
enum YJStackError: ErrorType {
/// 上溢
case Overflow
/// 下溢
case Underflow
}
/// 栈
class YJStack {
/// 栈,10个位置
private var list = [Int](count: 10, repeatedValue: 0)
/// 最新元素的位置
private var top = -1
}
如代码所示,我们使用list数组模拟栈,并制定栈中只能存放10个数据。在YJStackError的错误信息表示上溢和下溢。下图为大家演示了压入和弹出操作。
1.2.2 空栈
当top=-1时,栈中不包含任何元素,即栈是空的。要测试一个栈是否为空的使用empty(方法)。
// MARK: 栈是否为空
/// 栈是否为空
///
/// - returns: Bool
func empty() -> Bool {
return self.top == -1
}
1.2.3 弹出(pop)
如果试图对一个空栈执行弹出(pop)操作,则称栈下溢(underfolow),这通常是一个错误。
// MARK: 弹出
/// 弹出
///
/// - returns: Int throws YJStackError
func pop() throws ->Int {
guard !self.empty() else {
throw YJStackError.Underflow // 下溢
}
self.top--
return list[self.top+1]
}
1.2.4 压入(push)
如果在压入(push)的时候top超过了list数组的最大下标,即
top > list.count-1
则称栈上溢(overflow)。
// MARK: 压入
/// 压入
///
/// - returns: Int throws YJStackError
func push(x:Int) throws {
guard self.top + 1 != self.list.count else {
throw YJStackError.Overflow // 上溢
}
self.top++
list[self.top] = x
}
执行栈的操作时间都为O(1)。
2 队列
2.1 队列简介
队列上的insert操作称为入队(enqueue),delete操作称为出对(dequeue);正如栈的pop操作一样,dequeue操作也没有元素参数。
队列的先进先出特性类似于收银前台排队等待结账的一排顾客。队列有队头(head)和队尾(tail),当有一个元素入队时,它被放在队尾的位置,就像一个新来顾客排在队伍末端一样。而出对的元素则总是在队头的那个,就像排在队伍前面等待最久的那个顾客一样。
2.2 设计队列
2.2.1 队列YJQueue
//
// YJQueue.swift
// BasicDataStruct
//
// CSDN:http://blog.csdn.net/y550918116j
// GitHub:https://github.com/937447974/Blog
//
// Created by yangjun on 15/11/17.
// Copyright © 2015年 阳君. All rights reserved.
//
import Foundation
enum YJQueueError: ErrorType {
/// 上溢
case Overflow
/// 下溢
case Underflow
}
/// 队列
class YJQueue {
/// 队列,10个位置
private var list = [Int](count: 8, repeatedValue: 0)
/// 队头
private var head = 0
/// 队尾
private var tail = 0
}
我们使用list作为一个队列,最多容纳8个数。该队列有一个head指向队头元素。而属性tail则指向下一个元素将要插入的位置。队列中的元素存放位置为list[head..tail-1],最后head和tail都会移动到第10个位置,此时无法再插入,会发现有8个位置空余了,不利于队列的利用。我们采用环形队列重复利用这8个位置,如下图所示。
这样当head=tail则会有两种情况,一种是空,一种是满。为了解决这种问题,我们在YJQueue添加属性count。
/// 队列中存在的个数
private var count = 0
2.2.2 空队列
当count为0时,队列为空。
// MARK: 队列是否为空
/// 队列是否为空
///
/// - returns: Bool
func empty() -> Bool {
return self.count == 0
}
2.2.3 入队(enqueue)
如果self.count = self.list.count,此时队列是满的,此时若视图插入一个元素,则队列发生上溢。
// MARK: 入队
/// 入对
///
/// - returns: Int throws YJQueueError
func enqueue(x:Int) throws {
guard self.count != self.list.count else {
throw YJQueueError.Overflow // 上溢
}
list[self.tail] = x // 存数
self.count++ // 计数加1
self.tail++ // 后移动
self.tail = self.tail == self.count ? 0 : self.tail // 循环
}
2.2.4 出队(dequeue)
队列是空时,试图从空队列中删除一个元素,则队列发生下溢。
// MARK: 出队
/// 出队
///
/// - returns: Int throws YJQueueError
func dequeue() throws ->Int {
guard !self.empty() else {
throw YJQueueError.Underflow // 下溢
}
let temp = self.list[self.head]
self.count--
self.head++
self.tail = self.tail == self.count ? 0 : self.tail
return temp
}
执行队列的操作时间都为O(1)。
3 链表
3.1 简介
链表(linked list)是一种这样的数据结构,其中的各对象按线性顺序排列。数组的线性顺序是由数组下标决定的,然而和数组不同的是,链表的顺序是由各个对象里的指针决定的。链表为动态集合提供了一种简单而灵活的表示方法。
双向链表(doubly linked list)L的每个元素都是一个对象,每个对象有一个关键字key和两个指针:next和prev。对象中还可以包含其他的辅助数据(或卫星数据)。设x为链表的一个元素,x的next指向它在链表的后继元素,x的prev指向它的前驱元素。
如果x.prev=nil,则元素x没有前驱,因此是链表的第一个元素,即链表的头(head)。如果x.next=nil,则元素x没有后继,因此是链表的最后一个元素,即链表的尾(tail)。属性L.head指向链表的第一个元素。如果L.head=nil,则链表为空。
链表可以有多种形式。它可以是单链接的或双链接的,可以是已排序的或未排序的,可以是循环的或非循环的。
- 如果一个链表是单链接的,则省略每个元素中的prev指针。
- 如果链表是已排序(sorted)的,则链表的线性顺序与链表中关键字的线性顺序一致;据此,最小的元素就是表头元素,而最大的元素就是表尾元素。
- 如果链表是未排序(unsorted),则各元素可以以任何顺序出现。
- 在循环链表(circular list)中,头元素的prev指针指向表尾元素,而表尾元素的next指针则指向表头元素。我们可以将循环链表想象成一个各元素组成的圆环。
3.2 链表的优势
相比较普通的线性结构,链表结构的优势有:
- 单个节点创建非常方便,普通的线性内存通常在创建的时候就需要设定数据的大小。
- 节点的删除非常方便,不需要像线性结构那样移动剩下的数据。
- 节点的访问方便,可以通过循环或者递归的方法访问到任意数据,但是平均的访问效率低于线性表。
3.3 设计链表
3.3.1 链表中的元素YJListItem
我们设计的链表是未排序的双向链表。根据相关属性设计链表中的元素。
//
// YJListItem.swift
// BasicDataStruct
//
// CSDN:http://blog.csdn.net/y550918116j
// GitHub:https://github.com/937447974/Blog
//
// Created by yangjun on 15/11/17.
// Copyright © 2015年 阳君. All rights reserved.
//
import Foundation
/// 链表中的元素
class YJListItem {
/// 关键字
var key: String?
/// 前继元素
var prev: YJListItem!
/// 后继元素
var next: YJListItem!
}
3.3.2 链表YJList
这里使用YJList封装链表。
//
// YJList.swift
// BasicDataStruct
//
// CSDN:http://blog.csdn.net/y550918116j
// GitHub:https://github.com/937447974/Blog
//
// Created by yangjun on 15/11/17.
// Copyright © 2015年 阳君. All rights reserved.
//
import Foundation
/// 链表
class YJList {
/// 哨兵
private var sentinel = YJListItem()
// MARK: - 初始化
init() {
// 哨兵自循环
self.sentinel.next = self.sentinel
self.sentinel.prev = self.sentinel
}
}
这里你会发现我没有使用表头和表尾。这里设计了一个哨兵(sentinel)对象,其作用是简化边界条件的处理,让代码更加简单。
3.3.3 链表的搜索
过程search(key:String)采用简单的线性搜索方法,用于查找链表YJListItem中第一个关键字的元素,并返回该元素。最坏运行时间为O(n)。
// MARK: 查找YJListItem
/// 根据关键字查找YJListItem
///
/// - parameter key : 关键字
///
/// - returns: key
func search(key:String) -> YJListItem {
var x = self.sentinel.next
while x.key != nil && x.key != key {
x = x.next
}
return x
}
3.3.4 链表的插入
给定一个关键字key,方法insert(key:String)将key生成的item连接到链表的前端哨兵后面。运行时间为O(1)。
// MARK: 插入
/// 插入关键字
///
/// - parameter key : 关键字
///
/// - returns: void
func insert(key:String) {
let item = YJListItem()
item.key = key
item.next = self.sentinel.next
self.sentinel.next.prev = item
self.sentinel.next = item
item.prev = self.sentinel
}
3.3.5 链表的删除
方法delete(key:String)将一个关键字key对应的元素从链表YJListItem移除。该过程只需修改其前后元素的指针即可。最坏情况下需要的时间为O(n)。
// MARK: 删除
/// 删除关键字
///
/// - parameter key : 关键字
///
/// - returns: void
func delete(key:String) {
// 查找元素
let item = self.search(key)
if item.key != nil { // 存在则删除
item.prev.next = item.next
item.next.prev = item.prev
}
}
4 小结
本篇博文讲解了一些基本数据结构,其中主要有栈、队列和链表。其中队列介绍了循环队列;链表介绍了包含哨兵的双向无序链表。
其他
源代码
https://github.com/937447974/Algorithms
参考资料
算法导论
文档修改记录
时间 | 描述 |
---|---|
2015-11-17 | 完成关于栈和队列的项目 |
2015-11-16 | 完成关于链表的项目和博文 |