第十七章:经典抽象数据类型
Github
链接:ch17. 经典抽象数据类型
抽象数据类型 (ADT)
是非常常用的,最为常见的就是数组、顺序表、链表、栈和队列等等。诸如 OS
内部的任务调度有队列、双向链表、红黑树等均被广泛应用。熟练掌握各种数据结构是非常重要且必要的。
本章总结及注意点
部分课后习题解答
17.9 问题
-
栈。
-
队列。
-
当然可以。程序员封装即可。
top()
取栈顶元素但不进行栈顶元素的出栈,pop()
函数进行栈顶元素的出栈。 -
并不觉得有多强大。对于静态数组模拟的堆栈来讲,只需要将
top_element() = -1
这样进行赋值即能达到栈清空的效果。当然,C++ STL
容器中有单独的clear
函数用来清空容器。 -
这完全取决于它的初始化是 -1 还是 0。
-
首先
assert()
主要在此用于栈判空、判满,其实也就是为了不让数组出现越界访问。若删除所有断言,则相当于数组可能会产生越界访问,这两者等价。 -
链式堆栈中节点都是单独申请的,所以得单独释放。且
pop()
函数中已经实现了内存的释放,所以直接栈判空+pop()
即可。 -
肯定不可以!这是个常见问题,当
malloc()
空间被free
后,对应指针不可再访问一个已经被释放的内存空间。所以拿个临时指针变量保存其指向的空间地址,最后释放这个指针变量指向的空间即可。 -
答案给出了一个很不错的解释:
-
书中提到过。采用计数器确实比较简单,而空出一个数组元素不使用,则造成了空间浪费,当元素的大小较大时,这个空间浪费就越大。
-
这确实是一个比较麻烦的问题,总共会产生四种情况,队列为空、为满、
front
在前、在后。注意最后需要给算得的元素进行取模,当队列为空时,取模运算也会给出正确答案。见demo01.c
。 -
队列又不需要反向遍历,双向链表在此并不适用,
STL
中deque
双端队列,就慢的鬼一样… -
BST
插入只会在其叶节点位置插入,不会修改原来树的结构,这点注意下就行了。 -
和数组、链表相当, O ( n ) O(n) O(n)。
-
简单问题。
-
中序:左-根-右。后序:左-右-根。前序:根-左-右。其实能够发现:将前序遍历简单修改一下,变成:根-右-左,那么前序的逆序就是后序遍历。当然,我在此说的是迭代的写法,至于递归没啥好写的。可以见博文,四种遍历详细总结过:[M二叉树] lc145. 二叉树的后序遍历(栈+dfs)。
-
同上。简单看下参考答案吧:
-
中序遍历。四大遍历中没有可以直接得到降序序列的,但是简单修改中序遍历即可,另起变为 右-根-左,即可得到降序序列。
-
当然是后序遍历了,其在处理根节点之前,会将子节点处理完毕。
17.10 编程练习
-
这个函数必须为新的堆栈分配空间,并将旧堆栈的值复制到新的。然后它必须释放旧数组。断言用于确保新数组很大足够保存栈上当前的所有数据。见
demo02.c
。 -
此模块被转换为
stack
模块。resize
函数更有趣:并不是数组中的每个位置都需要复制,而且当数据绕到数组的末尾时,front
和rear
很容易变得不正确。见demo03.c
。 -
懒得写了,自行写完看看课后答案即可。就是简单的链表尾插和头删的基本操作。
-
这是简单的,堆栈数据被简单地声明为数组,作为参数传递的堆栈号选择要操作的元素。见
demo04.h、demo05.c
。 -
简单的
dfs()
,统计二叉树的节点数量。 -
常见的
bfs()
模板问题,在这是数组形式实现的BST
,也可参考我的博文 [M二叉树] lc102. 二叉树的层序遍历(队列+bfs)。 -
没啥意思,中序序列是否为严格升序即可。拿递归直接判断也可以,力扣上这题肯定是有的,上去练手就行了,在这还得建树、写测试用例还测不全。
-
我的博文:[C++系列] 76. 详解BST二叉搜索树。数组形式实现大同小异。删除的四种情况需要额外注意,找不到该值,找到且为叶子节点,找到有单独的孩子节点,找到有两个孩子节点。还要找左子树中的最大值进行值替换,这个应该是前驱节点,最后删除这个前驱节点即可。
-
这最好使用后序遍历。不幸的是,遍历函数的接口设计传递一个指向节点的值,而不是一个指向该节点包含的值,所以我们必须编写自己的。
-
同 8。
-
书中采用一个宏使堆栈具备可声明多个、解决命名冲突、泛型等问题。但是还是瑕疵,例如同类型的函数命名将会一致等。在此将堆栈功能分解为三个宏,解耦合,很不错的想法。且在日常学习中针对宏写的还是太少了,尤其是利用宏来写函数功能,更是少之又少,见一个得积累一个。见
demo08.c
随笔
-
栈的三种实现方式。静态数组、动态数组、链式栈实现。一般在算法题中会选择静态数组的方式来进行模拟实现。动态数组实现涉及到内存分配函数的使用,以及一定要防止的内存泄露问题。链式栈的空间利用率相当高,但是涉及大量的改变指针指向的相关操作,我个人觉得效率上来讲肯定没有静态数组高!
-
队列的三种实现方式,和栈相同。但是由于静态数组实现队列的情况下会造成大量的空间浪费,所以采用循环数组的方式来进行优化,我们在此一般称其为循环队列。采用两个指针加一个空余数组元素进行判空、判满。其实采用一个计数变量也是相当香的。循环队列两个指针,其中
rear
指针初始化为 0,是为了在添加第一个元素的时候能和front
指针指向同一个位置。故每次push
元素的时候都是先将rear
指针先向后移动一位再对该位置进行赋值。判空判满的两个式子也是相当巧妙:(rear+1)%SIZE==front
即队列已空,这个可以从初始化中就可以看出来,一开始队列为空的时候front
是 1,rear
是 0。当rear == front
的时候,整个队列中就只有一个元素,再出队后front++
则在rear
的前面一个位置,所以上式判空成立。且由于rear
和front
中至少间隔一个空元素,那么当(read+2)%SIZE == front
的时候说明这个队列已经满了。动态队列书中未实现,链式队列比较简单。 -
树,在此书中着重讲解了
BST
树,可参考我的博文,拿C++
实现的:[C++系列] 76. 详解BST二叉搜索树,同在该专栏下讲解了AVL
树和红黑树。链式BST
蛮不错的,消除了数组空间利用不充分的问题。其中,P377
链式二叉树的插入函数采用了两个指针,其中一个一级指针、一个二级指针,一级指针存储当前遍历到的树中的节点,二级指针指向当前节点的左右孩子指针指向的空间。二叉搜索树下所有的插入都只会在叶子节点中进行插入。 -
实现的改进提出了
ADT
的三个问题:用户声明多个堆栈、支持泛型、解决命名冲突问题。在C++ STL
中,有了模板,这些问题自然迎刃而解。在C
语言中可以采用宏来解决这个问题。在ch14
中就已经提到过了:宏是类型无关的。并且实现了一个支持任意类型的malloc
函数。书中运用宏参数实现类型无关,将堆栈代码写成了一个宏,且添加了用户可以自定义的命名标识,用##
的方式加到函数名称的后面。用C
语言来实现泛型是相当困难的,然而面向对象的语言对泛型是具备良好的支持的。
疑问
-
链式实现栈、队列、
BST
等其实都比较生疏,以往确实没有写过。静态数组是写的最多的。 -
关于利用宏来实现
C
语言下的泛型是值得考虑学习研究的事情! -
数据结构就得多刷题。