基本数据结构
2015-11-18 11:12
447 查看
常用的数据结构有:栈、队列、链表和有根树。本篇博文向大家介绍了栈、队列和链表,关于有根树会在后面关于树的算法中介绍。
栈和队列都是动态集合,且在其上进行delete操作所移除的元素都是预先设定的。在栈中,被删除的是最近插入的元素:栈实现的是一种后进先出策略。类似地,在队列中,被删除的总是在集合中存在时间最长的那个元素:队列实现的是一种先进先出策略。
如代码所示,我们使用list数组模拟栈,并制定栈中只能存放10个数据。在YJStackError的错误信息表示上溢和下溢。下图为大家演示了压入和弹出操作。
top > list.count-1
则称栈上溢(overflow)。
执行栈的操作时间都为O(1)。
队列的先进先出特性类似于收银前台排队等待结账的一排顾客。队列有队头(head)和队尾(tail),当有一个元素入队时,它被放在队尾的位置,就像一个新来顾客排在队伍末端一样。而出对的元素则总是在队头的那个,就像排在队伍前面等待最久的那个顾客一样。
我们使用list作为一个队列,最多容纳8个数。该队列有一个head指向队头元素。而属性tail则指向下一个元素将要插入的位置。队列中的元素存放位置为list[head..tail-1],最后head和tail都会移动到第10个位置,此时无法再插入,会发现有8个位置空余了,不利于队列的利用。我们采用环形队列重复利用这8个位置,如下图所示。
这样当head=tail则会有两种情况,一种是空,一种是满。为了解决这种问题,我们在YJQueue添加属性count。
执行队列的操作时间都为O(1)。
双向链表(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指针则指向表头元素。我们可以将循环链表想象成一个各元素组成的圆环。
单个节点创建非常方便,普通的线性内存通常在创建的时候就需要设定数据的大小。
节点的删除非常方便,不需要像线性结构那样移动剩下的数据。
节点的访问方便,可以通过循环或者递归的方法访问到任意数据,但是平均的访问效率低于线性表。
这里你会发现我没有使用表头和表尾。这里设计了一个哨兵(sentinel)对象,其作用是简化边界条件的处理,让代码更加简单。
GitHub:https://github.com/937447974/Blog
栈和队列都是动态集合,且在其上进行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 | 完成关于链表的项目和博文 |
版权所有
CSDN:http://blog.csdn.net/y550918116jGitHub:https://github.com/937447974/Blog
相关文章推荐
- 二叉树的遍历(数据结构)
- 最小生成树 prim算法实现(利用图的邻接矩阵来存放图)
- leetcode之Basic Calculator
- 【郝斌数据结构自学笔记】14-15_链表的重要性_typedef的用法
- HDU 2586 How far away ?(LCA)
- 数据结构与算法(1)--指针复习
- 无权最短路径
- 最短路径算法
- 【郝斌数据结构自学笔记】12-13_连续存储数组的算法演示
- 拓扑排序
- 数据结构(Java)——迭代器和列表的实例
- 图论算法基础
- 旭说数据结构之二叉树
- 数据结构之二叉树
- java中的各个数据结构区别
- Python学习记录-3-简明Python教程-数据结构
- 数据结构(Java)——迭代器Iterator
- 散列表(HashTable)
- 项目5-- 迷宫问题之图深度优先遍历解法
- ACM常用模板——数据结构——线段树