本文通过模拟汇编里的stack机制,构建一个自己的stack,然后将上一篇blog末尾的递归函数void bst_walk(bst_node_t *root)非递归化。

o libstack.h

 #ifndef _LIBSTACK_H
#define _LIBSTACK_H #ifdef __cplusplus
extern "C" {
#endif typedef void * uintptr_t; /* generic pointer to any struct */ uintptr_t *stack_init(size_t size);
void stack_fini();
int stack_isFull();
int stack_isEmpty();
void push(uintptr_t e);
void pop(uintptr_t *e); #ifdef __cplusplus
}
#endif #endif /* _LIBSTACK_H */

o libstack.c

 #include <stdio.h>
#include <stdlib.h>
#include "libstack.h" /**
* Basic Stack OPs are supported, including:
*
* 1. Construct/Destruct a stack
* 2. Tell stack is full or empty
* 3. push() and pop()
*
* == DESIGN NOTES ==
*
* There are 3 static variables reserved,
*
* ss: stack segment
* sp: stack pointer
* sz: stack size
*
* And the stack looks like:
*
* | RED | ss[-1] ; SHOULD NEVER BE ACCESSED
* low-addr +-----+ <------------TopOfStack-----------
* ^ | | ss[0]
* | | | ss[1]
* | | ... |
* | | |
* | | | ss[sz-1]
* | +-----+ <------------BottomOfStack--------
* high-addr | RED | ss[sz] ; SHOULD NEVER BE ACCESSED
*
* (1) If (sp - ss) == 0, stack is full
* (2) If (sp - ss) == sz, stack is empty
* (3) Push(E): { sp -= 1; *sp = E; }
* (4) Pop(&E): { *E = *sp; sp += 1; }
*/ static uintptr_t *ss = NULL; /* stack segment */
static uintptr_t *sp = NULL; /* stack pointer */
static size_t sz = ; /* stack size */ int stack_isFull() { return (sp == ss); }
int stack_isEmpty() { return (sp == ss + sz); } uintptr_t *
stack_init(size_t size)
{
ss = (uintptr_t *)malloc(sizeof (uintptr_t) * size);
if (ss == NULL) {
fprintf(stderr, "failed to malloc\n");
return NULL;
} sz = size;
sp = ss + size;
return ss;
} void
stack_fini()
{
free(ss);
} void
push(uintptr_t e)
{
sp -= ;
*sp = e;
} void
pop(uintptr_t *e)
{
*e = *sp;
sp += ;
}

1. 一旦栈被初始化后,栈指针sp一定是指向栈底,*sp不可访问(尤其是写操作),因为不在分配的内存有效范围内;

2. 对于入栈操作(push), 第一步是将sp-=1, 第二步是写入要入栈的元素 (*sp = E); (因为初始化后*sp的内存不可写,所以push操作一定率先改写sp)

3. 对于出栈操作(pop), 顺序与push相反,第一步取出sp指向的内存地址里的内容(E = *sp), 第二步才是将sp+=1;

o foo.c (简单测试)

 /**
* A simple test against stack OPs, including:
* o stack_init(), stack_fini()
* o stack_isFull(), stack_isEmpty()
* o push(), pop()
*/ #include <stdio.h>
#include "libstack.h" static void
dump_stack(uintptr_t *ss, size_t size)
{
(void) printf("%p: ", ss);
for (int i = ; i < size; i++) {
if (ss[i] != NULL)
(void) printf("%-10p ", *(ss+i));
else
(void) printf("0x%-8x ", 0x0);
}
printf("\n");
} int
main(int argc, char *argv[])
{
size_t size = ; uintptr_t *ss = stack_init(size);
dump_stack(ss, size); for (int i = ; !stack_isFull(); i++) {
push((uintptr_t)(ss+i));
dump_stack(ss, size);
} (void) printf("\n"); uintptr_t e = NULL;
for (; !stack_isEmpty();) {
pop(&e);
(void) printf(" (pop) got %-10p\n", e);
} stack_fini(); return ;
}

o Makefile

 CC      = gcc
CFLAGS = -g -Wall -std=gnu99 -m32
INCS = TARGET = foo all: ${TARGET} foo: foo.o libstack.o
${CC} ${CFLAGS} -o $@ $^ foo.o: foo.c
${CC} ${CFLAGS} -c $< ${INCS} libstack.o: libstack.c libstack.h
${CC} ${CFLAGS} -c $< clean:
rm -f *.o
clobber: clean
rm -f ${TARGET}

o 编译和运行测试

$ make
gcc -g -Wall -std=gnu99 -m32 -c foo.c
gcc -g -Wall -std=gnu99 -m32 -c libstack.c
gcc -g -Wall -std=gnu99 -m32 -o foo foo.o libstack.o $ ./foo
0x8ecc008: 0x0 0x0 0x0 0x0
0x8ecc008: 0x0 0x0 0x0 0x8ecc008
0x8ecc008: 0x0 0x0 0x8ecc00c 0x8ecc008
0x8ecc008: 0x0 0x8ecc010 0x8ecc00c 0x8ecc008
0x8ecc008: 0x8ecc014 0x8ecc010 0x8ecc00c 0x8ecc008 (pop) got 0x8ecc014
(pop) got 0x8ecc010
(pop) got 0x8ecc00c
(pop) got 0x8ecc008
$

测试简单且直接,不解释。如果还不确信,可以用gdb调试。

现在对上一篇blog末尾的递归函数使用上面实现的stack进行去递归化改写,改写后的代码如下:

 void
bst_walk(bst_node_t *root)
{
if (root == NULL)
return; (void) stack_init(STACK_SIZE); while (root != NULL || !stack_isEmpty()) {
if (root != NULL) {
push((uintptr_t)root);
root = root->left;
continue;
} pop((uintptr_t *)(&root));
printf("%d\n", root->key); root = root->right;
} stack_fini();
}

为方便阅读,下面给出使用meld进行diff后的截图,

  • L7: 构建一个stack, 其中STACK_SIZE是一个宏
  • L22: 将stack销毁
  • L9-14: 首先遍历左子树,不断将结点压入栈中,直到到达最左的叶子结点,那么则执行L16-17 (最左的叶子结点也会被压入栈中)
  • L16-17: 出栈并打印结点的key
  • L19: 将新的根结点设置为刚刚出栈的结点的右儿子, 重新执行L9-17, 直到所有结点都被遍历到(当然, stack为空)

注: 左图中的函数使用了两次递归,所以将其转化成非递归函数的难度相对较大。

将递归函数非递归化的一般方法(cont)的更多相关文章

  1. 在二叉搜索树(BST)中查找第K个大的结点之非递归实现

    一个被广泛使用的面试题: 给定一个二叉搜索树,请找出其中的第K个大的结点. PS:我第一次在面试的时候被问到这个问题而且让我直接在白纸上写的时候,直接蒙圈了,因为没有刷题准备,所以就会有伤害.(面完的 ...

  2. 二叉树系列 - [LeetCode] Symmetric Tree 判断二叉树是否对称,递归和非递归实现

    Given a binary tree, check whether it is a mirror of itself (ie, symmetric around its center). For e ...

  3. Java遍历文件夹的两种方法(非递归和递归)

    import java.io.File; import java.util.LinkedList; public class FileSystem {    public static int num ...

  4. 二叉树中序遍历,先序遍历,后序遍历(递归栈,非递归栈,Morris Traversal)

    例题 中序遍历94. Binary Tree Inorder Traversal 先序遍历144. Binary Tree Preorder Traversal 后序遍历145. Binary Tre ...

  5. 字符串拷贝函数递归与非递归的C语言实现

    初学递归的时候,觉得很抽象,不好分析,确实如此,尤其是有些时候控制语句不对,导致程序进去无限次的调用,更严重的是栈溢出.既要正确的控制结束语句,又要有正确的进入下次递归的语句,还要有些操作语句.... ...

  6. 算法笔记_013:汉诺塔问题(Java递归法和非递归法)

    目录 1 问题描述 2 解决方案  2.1 递归法 2.2 非递归法 1 问题描述 Simulate the movement of the Towers of Hanoi Puzzle; Bonus ...

  7. 非递归创建二叉树( C++队列 )

    非递归按照 层序 创建二叉树,利用 队列(即可先进先出特点)存放已访问的结点元素的地址. 初始化:front=rear= -1: 每储存一个结点元素 rear+1 ,利用 rear%2==0 来使 f ...

  8. 排序算法练习--JAVA(插入、直接选择、冒泡、快速排序、非递归快速排序)

    排序算法是数据结构中的经典算法知识点,也是笔试面试中经常考察的问题,平常学的不扎实笔试时候容易出洋相,回来恶补,尤其是碰到递归很可能被问到怎么用非递归实现... package sort; impor ...

  9. C语言实现 二分查找数组中的Key值(递归和非递归)

    基本问题:使用二分查找的方式,对数组内的值进行匹配,如果成功,返回其下标,否则返回 -1.请使用递归和非递归两种方法说明. 非递归代码如下: #include <stdio.h> int ...

随机推荐

  1. linux $ 类型变量 及Makefile 中 $ 类型变量的含义

    Shell 命令中: $$: shell pid $!: pid of the last process running in shell $?: shell command return code ...

  2. Web前端开发的一点记录

    工欲善其事必先利其器,开发工具选择Sublime Text 简称(ST) 本文所说的均在Windows NT 环境下使用的ST3运行. ST的Package Control安装方法: 1. 直接输入p ...

  3. C++一些注意点之转换操作符

    转换操作符定义 类可通过一个实参调用的非explicit构造函数定义一个隐式转换(其他类型—>类类型).当提供了实参类型的对象而需要一个类类型的对象时,编译器将使用该转换.这种构造函数定义了到类 ...

  4. 取得ASKII码值和汉语拼音

    aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXMAAACmCAIAAACnXPjtAAAgAElEQVR4nO2de3wb1YHv56+7e/fe/e ...

  5. asp.net 加入验证码

    验证码生成页面代码(清理掉没用的html) using System; using System.Collections.Generic; using System.Linq; using Syste ...

  6. SSL协议的握手过程

    SSL握手的目的 第一,客户端与服务器需要就一组用于保护数据的算法达成一致. 第二,它们需要确立一组由那些算法所使用的加密密钥. 第三,握手还可以选择对客户端进行认证. SSL 握手概述 SSL 握手 ...

  7. Linux网络编程(六)

    网络编程中,使用多路IO复用的典型场合: 1.当客户处理多个描述字时(交互式输入以及网络接口),必须使用IO复用. 2.一个客户同时处理多个套接口. 3.一个tcp服务程序既要处理监听套接口,又要处理 ...

  8. ASP.NET页面之间传递值的几种方式(转载)

    页面传值是学习asp.net初期都会面临的一个问题,总的来说有页面传值.存储对象传值.ajax.类.model.表单等.但是一般来说,常用的较简单有QueryString,Session,Cookie ...

  9. JS左侧菜单-03

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  10. uploadify的使用

    uploadify的使用 课程设计需要实现上传文件模块,本来ASP.NET是有内置的控件,但是ASP.NET MVC没有,所以就有两种方法:自定义和采用第三方插件.由于时间的关系,故采用第三方插件:u ...