【程序人生】数据结构杂记(三)
说在前面
个人读书笔记
栈
栈(stack)是存放数据对象的一种特殊容器,其中的数据元素按线性的逻辑次序排列,故也可定义首、末元素。不过,尽管栈结构也支持对象的插入和删除操作,但其操作的范围仅限于栈的某一特定端。也就是说,若约定新的元素只能从某一端插入其中,则反过来也只能从这一端删除已有的元素。禁止操作的另一端,称作盲端。
栈中元素接受操作的次序必然始终遵循所谓“后进先出”(last-in-first-out, LIFO)的规律:
从栈结构的整个生命期来看,更晚(早)出栈的元素,应为更早(晚)入栈者;反之,更晚(早)入栈者应更早(晚)出栈。
栈中可操作的一端更多地称作栈顶(stack top),而另一无法直接操作的盲端则更多地称作栈底(stack bottom)。
栈的典型应用——逆序输出
在栈所擅长解决的典型问题中,有一类具有以下共同特征:
- 首先,虽有明确的算法,但其解答却以线性序列的形式给出;
- 其次,无论是递归还是迭代实现,该序列都是依逆序计算输出的;
- 最后,输入和输出规模不确定,难以事先确定盛放输出数据的容器大小。因其特有的“后进先出”特性及其在容量方面的自适应性,使用栈来解决此类问题可谓恰到好处。
示例:进制转换
递归实现:
迭代实现:
栈的典型应用—— 递归嵌套
示例:括号匹配
实际上,只要将push、pop操作分别与左、右括号相对应,则长度为n的栈混洗,必然与由n对括号组成的合法表达式彼此对应。比如,栈混洗{ 3, 2, 4, 1 }对应于表达式"( ( ( ) ) ( ) )"。按照这一理解,借助栈结构,只需扫描一趟表达式,即可在线性时间内,判定其中的括号是否匹配。
实例:
算法:
栈的典型应用——逆波兰表达式
逆波兰表达式(reverse Polish notation,,RPN)是数学表达式的一种,其语法规则可概括为:
操作符紧邻于对应的(最后一个)操作数之后。比如
“
12
+
”
“1 2 +”
“12+”即通常习惯的
“
1
+
2
”
“1 + 2”
“1+2”。
按此规则,可递归地得到更复杂的表达式,比如RPN表达式
1 2 + 3 4 ^ *
即对应于常规的表达式
( 1 + 2 ) * 3 ^ 4
波兰表达式求值算法:
示例:
试探回溯法
八皇后问题
如图,国际象棋中皇后的势力范围覆盖其所在的水平线、垂直线以及两条对角线。现考查如下问题:
在
n
∗
n
n * n
n∗n的棋盘上放置
n
n
n个皇后,如何使得她们彼此互不攻击——此时称她们构成一个可行的棋局。
对于任何整数
n
>
=
4
n >= 4
n>=4,这就是
n
n
n皇后问题。
由鸽巢原理可知,在
n
n
n行
n
n
n列的棋盘上至多只能放置
n
n
n个皇后。反之,
n
n
n个皇后在
n
∗
n
n * n
n∗n棋盘上的可行棋局通常也存在。
皇后类:
既然每行能且仅能放置一个皇后,故不妨首先将各皇后分配至每一行。然后,从空棋盘开始,逐个尝试着将她们放置到无冲突的某列。每放置好一个皇后,才继续试探下一个。若当前皇后在任何列都会造成冲突,则后续皇后的试探都必将是徒劳的,故此时应该回溯到上一皇后。
示例:
迷宫寻径问题
间区域限定为由 n ∗ n n * n n∗n个方格组成的迷宫,除了四周的围墙,还有分布其间的若干障碍物;只能水平或垂直移动。我们的任务是,在任意指定的起始格点与目标格点之间,找出一条通路(如果的确存在)。
格点是迷宫的基本组成单位,故首先需要实现Cell类
可见,除了记录其位置坐标外,格点还需记录其所处的状态。共有四种可能的状态:
原始可用的(AVAILABLE)、在当前路径上的(ROUTE)、所有方向均尝试失败后回溯过的(BACKTRACKED)、不可穿越的(WALL)。属于当前路径的格点,,需记录其前驱和后继格点的方向。
既然只有上、下、左、右四个连通方向,故以EAST、SOUTH、WEST和NORTH区分。
特别地,因尚未搜索到而仍处于初始 AVAILABLE状态的格点,邻格的方向都是未知的(UNKNOWN);经过回溯后处于BACKTRACKED状态的格点,与邻格之间的连通关系均已关闭,故标记为NO_WAY。
在路径试探过程中需反复确定当前位置的相邻格点
在确认某一相邻格点可用之后,算法将朝对应的方向向前试探一步,同时路径延长一个单元。
基于试探回溯策略实现寻径算法:
从算法的中间过程及最终结果都可清晰地看出,这里用以记录通路的栈结构的确相当于忒修斯手中的线绳,它确保了算法可沿着正确地方向回溯。另外,这里给所有回溯格点所做的状态标记则等效于用粉笔做的记号,正是这些标记确保了格点不致被重复搜索,从而有效地避免了沿环路的死循环现象。
队列
与栈一样,队列(queue)也是存放数据对象的一种容器,其中的数据对象也按线性的逻辑次序排列。队列结构同样支持对象的插入和删除,但两种操作的范围分别被限制于队列的两端——若约定新对象只能从某一端插入其中,则只能从另一端删除已有的元素。允许取出元素的一端称作队头(front),而允许插入元素的另一端称作队尾(rear)。
由以上的约定和限制不难看出,与栈结构恰好相反,队列中各对象的操作次序遵循所谓先进先出(first-in-first-out, FIFO)的规律:
更早(晚)出队的元素应为更早(晚)入队者,反之,更早(晚)入队者应更早(晚)出队。
队列应用——循环分配器
为在客户(client)群体中共享的某一资源(比如多个应用程序共享同一CPU),一套公平且高效的分配规则必不可少,而队列结构则非常适于定义和实现这样的一套分配规则。
队列应用——银行服务模拟
结语
如果您有修改意见或问题,欢迎留言或者通过邮箱和我联系。
手打很辛苦,如果我的文章对您有帮助,转载请注明出处。