[C和指针] ch17. 经典抽象数据类型

第十七章:经典抽象数据类型

Github 链接:ch17. 经典抽象数据类型

抽象数据类型 (ADT) 是非常常用的,最为常见的就是数组、顺序表、链表、栈和队列等等。诸如 OS 内部的任务调度有队列、双向链表、红黑树等均被广泛应用。熟练掌握各种数据结构是非常重要且必要的。

本章总结及注意点

[C和指针] ch17. 经典抽象数据类型_第1张图片

[C和指针] ch17. 经典抽象数据类型_第2张图片


部分课后习题解答

17.9 问题

  1. 栈。

  2. 队列。

  3. 当然可以。程序员封装即可。top() 取栈顶元素但不进行栈顶元素的出栈,pop() 函数进行栈顶元素的出栈。

  4. 并不觉得有多强大。对于静态数组模拟的堆栈来讲,只需要将 top_element() = -1 这样进行赋值即能达到栈清空的效果。当然,C++ STL 容器中有单独的 clear 函数用来清空容器。

  5. 这完全取决于它的初始化是 -1 还是 0。

  6. 首先 assert() 主要在此用于栈判空、判满,其实也就是为了不让数组出现越界访问。若删除所有断言,则相当于数组可能会产生越界访问,这两者等价。

  7. 链式堆栈中节点都是单独申请的,所以得单独释放。且 pop() 函数中已经实现了内存的释放,所以直接栈判空+pop() 即可。

  8. 肯定不可以!这是个常见问题,当 malloc() 空间被 free 后,对应指针不可再访问一个已经被释放的内存空间。所以拿个临时指针变量保存其指向的空间地址,最后释放这个指针变量指向的空间即可。

  9. 答案给出了一个很不错的解释:

    [C和指针] ch17. 经典抽象数据类型_第3张图片

  10. 书中提到过。采用计数器确实比较简单,而空出一个数组元素不使用,则造成了空间浪费,当元素的大小较大时,这个空间浪费就越大。

  11. 这确实是一个比较麻烦的问题,总共会产生四种情况,队列为空、为满、 front 在前、在后。注意最后需要给算得的元素进行取模,当队列为空时,取模运算也会给出正确答案。见 demo01.c

  12. 队列又不需要反向遍历,双向链表在此并不适用,STLdeque 双端队列,就慢的鬼一样…

  13. BST 插入只会在其叶节点位置插入,不会修改原来树的结构,这点注意下就行了。

    [C和指针] ch17. 经典抽象数据类型_第4张图片

  14. 和数组、链表相当, O ( n ) O(n) O(n)

  15. 简单问题。

    [C和指针] ch17. 经典抽象数据类型_第5张图片

  16. 中序:左-根-右。后序:左-右-根。前序:根-左-右。其实能够发现:将前序遍历简单修改一下,变成:根-右-左,那么前序的逆序就是后序遍历。当然,我在此说的是迭代的写法,至于递归没啥好写的。可以见博文,四种遍历详细总结过:[M二叉树] lc145. 二叉树的后序遍历(栈+dfs)

  17. 同上。简单看下参考答案吧:

    [C和指针] ch17. 经典抽象数据类型_第6张图片

  18. 中序遍历。四大遍历中没有可以直接得到降序序列的,但是简单修改中序遍历即可,另起变为 右-根-左,即可得到降序序列。

  19. 当然是后序遍历了,其在处理根节点之前,会将子节点处理完毕。

17.10 编程练习

  1. 这个函数必须为新的堆栈分配空间,并将旧堆栈的值复制到新的。然后它必须释放旧数组。断言用于确保新数组很大足够保存栈上当前的所有数据。见 demo02.c

  2. 此模块被转换为 stack 模块。resize 函数更有趣:并不是数组中的每个位置都需要复制,而且当数据绕到数组的末尾时,frontrear 很容易变得不正确。见 demo03.c

  3. 懒得写了,自行写完看看课后答案即可。就是简单的链表尾插和头删的基本操作。

  4. 这是简单的,堆栈数据被简单地声明为数组,作为参数传递的堆栈号选择要操作的元素。见 demo04.h、demo05.c

  5. 简单的 dfs(),统计二叉树的节点数量。

  6. 常见的 bfs() 模板问题,在这是数组形式实现的 BST,也可参考我的博文 [M二叉树] lc102. 二叉树的层序遍历(队列+bfs)

    [C和指针] ch17. 经典抽象数据类型_第7张图片

  7. 没啥意思,中序序列是否为严格升序即可。拿递归直接判断也可以,力扣上这题肯定是有的,上去练手就行了,在这还得建树、写测试用例还测不全。

  8. 我的博文:[C++系列] 76. 详解BST二叉搜索树。数组形式实现大同小异。删除的四种情况需要额外注意,找不到该值,找到且为叶子节点,找到有单独的孩子节点,找到有两个孩子节点。还要找左子树中的最大值进行值替换,这个应该是前驱节点,最后删除这个前驱节点即可。

  9. 这最好使用后序遍历。不幸的是,遍历函数的接口设计传递一个指向节点的值,而不是一个指向该节点包含的值,所以我们必须编写自己的。

  10. 同 8。

  11. 书中采用一个宏使堆栈具备可声明多个、解决命名冲突、泛型等问题。但是还是瑕疵,例如同类型的函数命名将会一致等。在此将堆栈功能分解为三个宏,解耦合,很不错的想法。且在日常学习中针对宏写的还是太少了,尤其是利用宏来写函数功能,更是少之又少,见一个得积累一个。见 demo08.c

随笔

  1. 栈的三种实现方式。静态数组、动态数组、链式栈实现。一般在算法题中会选择静态数组的方式来进行模拟实现。动态数组实现涉及到内存分配函数的使用,以及一定要防止的内存泄露问题。链式栈的空间利用率相当高,但是涉及大量的改变指针指向的相关操作,我个人觉得效率上来讲肯定没有静态数组高!

  2. 队列的三种实现方式,和栈相同。但是由于静态数组实现队列的情况下会造成大量的空间浪费,所以采用循环数组的方式来进行优化,我们在此一般称其为循环队列。采用两个指针加一个空余数组元素进行判空、判满。其实采用一个计数变量也是相当香的。循环队列两个指针,其中 rear 指针初始化为 0,是为了在添加第一个元素的时候能和 front 指针指向同一个位置。故每次 push 元素的时候都是先将 rear 指针先向后移动一位再对该位置进行赋值。判空判满的两个式子也是相当巧妙:(rear+1)%SIZE==front 即队列已空,这个可以从初始化中就可以看出来,一开始队列为空的时候 front 是 1,rear 是 0。当 rear == front 的时候,整个队列中就只有一个元素,再出队后 front++ 则在 rear 的前面一个位置,所以上式判空成立。且由于 rearfront 中至少间隔一个空元素,那么当 (read+2)%SIZE == front 的时候说明这个队列已经满了。动态队列书中未实现,链式队列比较简单。

  3. 树,在此书中着重讲解了 BST 树,可参考我的博文,拿 C++ 实现的:[C++系列] 76. 详解BST二叉搜索树,同在该专栏下讲解了 AVL 树和红黑树。链式 BST 蛮不错的,消除了数组空间利用不充分的问题。其中,P377 链式二叉树的插入函数采用了两个指针,其中一个一级指针、一个二级指针,一级指针存储当前遍历到的树中的节点,二级指针指向当前节点的左右孩子指针指向的空间。二叉搜索树下所有的插入都只会在叶子节点中进行插入。

  4. 实现的改进提出了 ADT 的三个问题:用户声明多个堆栈、支持泛型、解决命名冲突问题。在 C++ STL 中,有了模板,这些问题自然迎刃而解。在 C 语言中可以采用宏来解决这个问题。在 ch14 中就已经提到过了:宏是类型无关的。并且实现了一个支持任意类型的 malloc 函数。书中运用宏参数实现类型无关,将堆栈代码写成了一个宏,且添加了用户可以自定义的命名标识,用 ## 的方式加到函数名称的后面。用 C 语言来实现泛型是相当困难的,然而面向对象的语言对泛型是具备良好的支持的。

疑问

  1. 链式实现栈、队列、BST 等其实都比较生疏,以往确实没有写过。静态数组是写的最多的。

  2. 关于利用宏来实现 C 语言下的泛型是值得考虑学习研究的事情!

  3. 数据结构就得多刷题。

你可能感兴趣的:(读书笔记,读书笔记)