算法导论 总结索引 | 第三部分 第十章:基本数据结构

第三部分:数据结构

1、不同的算法 可能需要 对集合执行不同的操作。例如,许多算法 只需要能在一个集合中 插入和删除元素,以及测试元素 是否属于集合。支持这些操作的动态集合 称为字典
其他一些算法需要更复杂的操作,例如:堆排序中 介绍的 最小优先队列,它支持 向集合中插入一个元素 和 从中取出一个最小元素的操作

2、动态集合的元素:一些类型的动态集合 假定对象中的一个属性 为标识关键字(key)。如果关键字 全不相同,可以 将动态集合 视为一个关键字值的集合。对象可能 包含卫星数据,它们 与其他对象属性 一起移动,除此之外,集合实现不使用它们。对象也可以 有由集合操作使用的属性;这些属性可能 包含有关集合中 其他对象的数据 或 指针

一些动态集合 以其关键字来自于某个全序集 为前提条件,例如,全序关系 允许定义一个集合的最小元素,也可以 确定比集合中一个给定元素大的 下一个元素

3、动态集合上的操作 可以分为两类:简单返回有关集合信息的查询操作 和 改变集合的修改操作

SUCCESSOR(S,x):一个查询操作,给定关键字属于全序集S的一个元素x,返回S中比x大的前一个元素的指针;如果x为最大元素,则返回NIL
PREDECESSOR(S, x):一个查询操作,给定关键字属于全序集S的一个元素x,返回S中比x小的前一个元素的指针;如果x为最小元素,则返回NIL

在某些情况下,能够将 SUCCESSOR 和 PREDECESSOR 查询操作 推广应用到一些具有相同关键字的集合上。对于一个有n个关键字的集合,通常的假设是 调用一次MAXIMUM后再调用 n - 1次 SUCCESSOR, 就可以 按序枚举出 该集合中的所有元素

1、栈和队列

1、栈和队列 都是动态集合,且在其上 进行 DELETE 操作所移除的元素是预先设定的

栈实现的是 一种后进先出策略;队列实现的是 一种先进先出策略

2、栈:栈上的INSERT操作称为压入 (PUSH),而无元素参数的 DELETE 操作 称为弹出 (POP)

可以用一个数组 S[1…n] 来实现 一个最多可容纳n个元素的栈。该数组 有一个属性 S.top,指向 最新插入的元素
当 S.top = 0(数组下标从1 开始)时,栈中不包含任何元素,即 栈是空的。要测试一个栈 是否为空 可以用查询操作 STACK-EMPTY。试图 对一个空栈 执行弹出操作,则 称栈下溢,这通常是个错误。如果 S.top 超过了 n,则称为 栈上溢(下面伪代码 不考虑栈的上溢问题)

STACK-EMPTY(S)
	if S.top == 0
		return TRUE
	else return FALSE

PUSH(S, x)
	S.top = S.top + 1
	S[S.top] = x

POP(S)
	if STACK-EMPTY(S)
		error "underflow"
	else S.top = S.top - 1
		return S[S.top + 1]

3、队列:队列上的 INSERT 操作 称为入队(ENQUEUE),DELETE 操作 称为出队(DEQUEUE); DEQUEUE 操作也没有元素参数

循环实现:利用数组 Q[1…n] 来实现 一个最多容纳 n - 1 个元素的队列的一种方式。该队列有一个属性 Q.head 指向队头元素,而属性 Q.tail 则指向 下一个新元素将要插入的位置

当 Q.head == Q.tail 时,队列为空。初始有 Q.head == Q.tail == 1
为了 处理上溢 需要舍弃一个元素,Q.head == Q.tail + 1 表示队满,Q.head == Q.tail 表示队空

ENQUEUE(Q, x)
    if Q.head == Q.tail + 1
        error "overflow"
    else Q[Q.tail] = x
        if Q.tail == Q.length
            Q.tail = 1
        else Q.tail = Q.tail + 1
 
DEQUEUE(Q)
    if Q.head == Q.tail
        error "underflow"
    else x = Q[Q.head]
        if Q.head = Q.length
            Q.head = 1
        else Q.head = Q.head + 1
        return x

4、双端队列(deque),其插人和删除操作都可以在两端进行。写出4个时间均为 O(1) 的过程,分别实现在双端队列的两端 插入和删除元素的操作,该队列 是用一个数组实现的

左头右尾,用完了到另一端头尾左右不变

HEAD_ENQUEUE(Q, x)
    if Q.head == Q.tail + 1
        error "overflow"
    else
        if Q.head == 1
            Q.head = Q.length
        else Q.head = Q.head - 1
        Q[Q.head] = x
 
TAIL_ENQUEUE(Q, x)
    if Q.head == Q.tail + 1
        error "overflow"
    else Q[Q.tail] = x
        if Q.tail == Q.length
            Q.tail = 1
        else Q.tail = Q.tail + 1
 
HEAD_DEQUEUE(Q)
    if Q.head == Q.tail
        error "underflow"
    else x = Q[Q.head]
        if Q.head == Q.length
            Q.head = 1 // 满了回到头上去取
        else Q.head = Q.head + 1
        return x
 
TAIL_DEQUEUE(Q)
    if Q.head == Q.tail
        error "underflow"
    else
        if Q.tail == 1
            Q.tail = Q.length
        else Q.tail = Q.tail - 1
        x = Q[Q.tail]
        return x

2、链表

1、其中的各对象 按线性顺序排列。数组的线性顺序 是由数组的下标决定的,然而 与数组不同的是,链表的顺序 是由各个对象里的指针决定的

2、双向链表 L的每个元素 都是一个对象,每个对象 有一个关键字key 和 两个指针:next 和 prev。对象中 还可以包含其他的辅助数据(或称卫星数据)

3、如果一个链表是单链接的,则省略 每个元素中的prev指针。如果链表是已排序的,则链表的线性顺序 与链表元素中关键字的线性顺序一致;据此 最小的元素 就是表头元素,而最大的元素 则是表尾元素

在循环链表中,表头元素的 prev指针 指向表尾元素,而表尾元素的 next指针 则指向表头元素

4、链表的搜索

LIST-SEARCH(L, k)
	x = L.head
	while x != NIL and x.key != k
		x = x.next
	return x

5、链表的删除:过程 LIST-DELETE 将一个元素x 从链表L中移除

LIST-DELETE(L, x)
	if x.prev != NIL
		x.prev.next = x.next
	else L.head = x.next
	if x.next != NIL
		x.next.prev = x.prev

6、哨兵是 一个哑对象,其作用是 简化边界条件的处理

有哨兵的双向循环链表,哨兵 L.nil 位于 表头和表尾之间。属性L.nil.next 指向表头,L.nil.prev 指向表尾
一个空的链表 只由一个哨兵构成,L.nil.next 和 L.nil.prev 同时指向 L.nil
可以通过 L.nil.next访问表头,属性 L.head 就不需要

有哨兵的 LIST-SEARCH

LIST-SEARCH(L, k)
	x = L.nil.next
	while x != L.nil and x.key != k
		x = x.next
	return x

有哨兵的 LIST-INSERT

LIST-INSERT(L, x)
	x.next = L.nil.next
	L.nil.next.prev = x
	L.nil.next = x
	x.prev = L.nil

哨兵基本不能 降低数据结构相关操作的 渐进时间界,但可以降低常数因子。在循环语句中 使用哨兵的好处 往往在于 可以使代码简洁

我们应当慎用哨兵,假如有许多个很短的链表,它们的哨兵 所占用的额外的存储空间会造成 严重的存储浪费

7、哨兵的 LIST-SEARCH 过程中的每一次循环迭代 都需要两个测试:一是检查 x != L.nil ,另一个是 检查 x.key != k。如何在每次迭代中 省略对 x != L.nil 的检查

哨兵是NIL,哨兵的key改成k,从 NIL下一个开始找

LIST-SEARCH'(L, k)
    x = L.nil.next
    L.nil.key = k
    while x.key ≠ k
        x = x.next
    return x

3、指针和对象的实现

1、在没有显式的指针数据类型的情况下 实现链式数据结构的两种方法,将利用 数组的下标来构造对象和指针
1)对象的多数组表示
对每一个属性 使用一个数组表示,用三个数组 来实现之前所述的链表:三个数组项 key[x]、next[x]和prev[x] 一起表示 链表中的一个对象

2)对象的单数组表示
一个对象 占用一段连续的子数组 A[j…k] ,对象中的每个属性 对应从0到k - j之间的一个偏移量,指向该对象的指针 就是下标j
给定一个指针i,要读取i.prev的值,只需要 在指针的值i上 加上偏移量2,所以要读取的是 A[i + 2]
对象的单数组表示
2、对象的分配与释放:向一个 双向链表 表示的动态集合中 插入一个关键字,就必须分配 一个指向该链表表示中 尚未利用的对象的指针
由垃圾收集器 负责确定 哪些对象是未使用的

假设 多数组表示法中的 各数组长度为m,且在 某一时刻 该动态集合含有 n<=m 个元素。则n个对象 代表存于 该动态集合中的元素,而 余下的 m - n 个对象是自由的;这些 自由对象可用来 表示将要 插入该动态集合的元素

把 自由对象保存在 一个单链表中,称为 自由表。自由表 只使用 next数组,该数组 只存储 链表中的next指针。该表示中的每一个对象 不是在链表L中,就在自由表中,但不会 同时属于两个表

自由表类似于 一个栈:下一个被分配的对象 就是最后被释放的那个。可以分别利用 栈操作 PUSH和POP的栈表实现形式 来实现 分配和释放对象的过程。假设 下述过程中的 全局变量 free指向自由表的 第一个元素

ALLOCATE-OBJECT()
	if free == NIL
		error "out of space"
	else x = free
		free = x.next
		return x;

FREE-OBJECT(x)
	x.next = free
	free = x

free数组
链表(浅阴影部分)和自由表(深阴影部分),箭头 标示 自由表的结构

甚至可以 让多个链表共用一个 自由表

可以将其改造,让对象中的 任意一个属性 都可以像 自由表的next属性一样使用,从而使 其可以对 任何同构的对象组 都适用
任意一个属性 都可以像 自由表的next属性一样使用
3、序列<13,4,8,19,5,11>
多数组表示的双向链表
多数组表示的双向链表
单数组表示的形式(pre+num+next)
单数组表示的形式

4、有根树的表示

1、用链式数据结构 表示有根树 的问题,在 二叉树T中 如何利用属性 p、left和right 存放指向 父节点、左孩子 和 右孩子的指针
用链式数据结构 表示有根树
2、二叉树的表示方法 可以推广到 每个结点的孩子数至多为常数k的 任意类型的树;只需要将left和night属性 用child1,child2, …, childk 代替。当孩子的结点数 无限制时,这种方法就失效了,因为我们不知道 应当预先分配多少个属性(在多数组表示法中 就是多少个数组)
即使孩子数k 限制在 一个大的常数以内,但若 多数结点只有少量的孩子,则会浪费大量存储

有一个巧妙的方法可以用来表示孩子数任意的树。该方法的优势在于,对任意个结点的有根树,只需要 O(n) 的存储空间,这种左孩子右兄弟表示法,每个节点 都包含一个 父节点指针p,且 T.root 指向树T的根结点。每个节点中 不是包含 指向每个孩子的指针,而是只有两个指针
1)x.left-child 指向 结点x最左边的孩子结点
2)x.right-child 指向 x右侧相邻的 兄弟结点

结点x没有 孩子结点,则 x.left-child = NIL;如果 结点x是 其父结点的 最右孩子,则 x.right-sibling = NIL
左孩子右兄弟表示法
3、树的其他表示方法:对一棵完全二叉树 使用 堆来表示,堆 用一个单数组 加上 堆的最末结点下标表示

4、左孩子右兄弟树的递归与非递归、深度与广度遍历
来自 https://blog.csdn.net/fycy2010/article/details/47406813

左孩子右兄弟存储方式:

struct tree_node
{
  int data;
  tree_node* first_child;
  tree_node* sibling;
}

1)深度遍历:
(1) 递归算法:
先根遍历

void preOrder(tree_node *root)
{
	if (!root)
		return;
	printf(root->data);
	tree_node* p;
	for (p = root->first_child; p; p = p->sibling)
	{
    	preOrder(p);
    }
}

后根遍历

void postOrder(tree_node *root)
{
	if (!root)
		return;
	tree_node* p;
	for (p = root->first_child; p; p = p->sibling)
	{
    	postOrder(p);
    }
    printf(root->data);
}

(2)非递归算法:
记忆技巧是 将节点看成是二叉树,因为树对应的二叉树的先序 与 树的先根遍历序列 是一致的

先根遍历

void preOrder(tree_node* root)
{
	stack sk;
    while(root || !sk.empty())
    {
    	while(root)
        {
	    	printf(root->data);
            sk.push(root);
            root = root->first_child;
        }
        root = sk.top(); sk.pop(); // STL要求这样做的
        root = root->sibling; // root可以为空的
	}
}

2)广度遍历(按层次次序遍历树的算法)
用队列实现

void levelOrder(tree_node* root)
{
	queue q;
	q.push(root);
    while (!q.empty())
    {
	    root = q.front(); q.pop();
        printf(root->data);
        root = root->first_child;
        while (!root)
        {
        	q.push(root);
            root = root->sibling;
        }
	}
}
  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值