/*
理解和记忆递归的关键在于把握递归的本质和函数调用的过程。递归函数在每次调用时会把当前状态压入调用栈,直到满足终止条件后开始回溯。理解基准条件和递归步骤:每个递归函数都需要有基准条件(如节点为空时返回),并在每一步中递归调用自身处理子问题。
*/
main.c
c
#include<stdio.h>
#include<stdlib.h>
#define NAMESIZE 32
struct score_st
{
int id;
char name[NAMESIZE];
int math;
int chinese;
};
struct node_st
{
struct score_st data;
struct node_st *l,*r;
};
print_s(struct score_st *d)
{
printf("%d %s %d %d\n",d->id,d->d.name,d->math,d->chinese);
}
int insert(struct node_st **root,struct score_st *data)
{
struct node_st *node;
if(*root == NULL)
{
node = malloc(sizeof(*node));
if(node == NULL)
return -1;
node->data = *data;
node->l = NULL;
node->r = NULL;
*root = node;
return 0;
}
if(data->id <= (*root)->data.id)
return insert(&(*root)->l,data);
else
return insert(&(*root)->r,data);
}
struct score_st *find(struct node_st *root,int id)
{
if(root == NULL)
return NULL;
if(id == root->data.id)
return &root->data;
if(id < root->data.id)
return find(root->l,id);
else
return find(root->r,id);
}
void draw_(struct node_st *root,in level)
{
int i;
if(root == NULL)
return ;
draw_(root->t,level+1);
for(i = 0;i<level;i++)
printf(" ");
print_s(&root->data);
draw_(root->l,level+1);
}
void draw(struct node_st *root)
{
draw_(root,0);
}
int main()
{
int arr[] = {1,2,3,7,6,5,9,8,4};
int i;
struct score_st tmp,*datap;
struct node_st *tree = NULL;
for(i = 0;i<sizeof(arr)/sizeof(*arr);i++)
{
tmp.id = arr[i];
snprintf(tmp.name,NAMESIZE,"stu%d",arr[i]);
tmp.math = rand()%100;
tmp.chinese = rand()%100;
insert(&tree,&tmp);
}
draw(tree);
#if 0
int tmpid = 2;
datap = find(tree,tmpid);
if(datap == NULL)
printf("can not find the id %d\n",tmpid);
else
print_s(datap);
#endif
exit(0);
}
补充说明1
为了理解二级指针的一级目标,我们需要明确指针和二级指针的概念。
指针和二级指针
- 指针 :一个指针变量保存了另一个变量的地址。通过指针,我们可以访问或修改这个变量的值。例如,
int *p
是一个指向整数的指针变量。 - 二级指针 :一个二级指针变量保存了另一个指针变量的地址。通过二级指针,我们可以访问或修改这个指针变量的值。例如,
int **pp
是一个指向指针变量的指针。
二级指针的一级目标
一级目标是指二级指针所指向的那个指针变量。
举个具体的例子,假设我们有以下定义:
c
int a = 10; // 普通变量
int *p = &a; // 指向变量 a 的指针
int **pp = &p; // 指向指针 p 的指针
在这个例子中:
p
是一个指向a
的指针。pp
是一个指向p
的指针。
我们可以这样理解:
pp
保存了p
的地址。*pp
解析出pp
所指向的内容,即p
。**pp
解析出*pp
所指向的内容,即a
的值。
当然可以!以下是一个图形示例来说明*aa = b
的含义:
假设:
aa
是一个指向指针的指针。a
是一个指针,它最初指向一个地址。b
是另一个指针,指向另一个地址。
初始状态:
+------+ +------+ +------+
| aa | -> | a | -> | x | (假设 x 是 a 所指向的值)
+------+ +------+ +------+
+------+ +------+
| b | -> | y | (假设 y 是 b 所指向的值)
+------+ +------+
执行 *aa = b
后的状态:
+------+ +------+
| aa | -> | b | -> | y | (aa 现在指向 b,而 b 仍然指向 y)
+------+ +------+
这样做后,aa
所指向的指针 a
不再指向原来的地址,而是指向了 b
所指向的地址。
以下是更详细的图示:
-
初始状态:
aa -> a -> [地址1: x]
b -> [地址2: y] -
执行
*aa = b
后:aa -> b -> [地址2: y]
这意味着,aa
原本指向的指针 a
,现在指向了 b
,即 a
的地址已经被 b
的地址覆盖。
希望这个图形示例能帮助你更好地理解 *aa = b
的含义。如果还有其他问题,请随时告诉我!
补充说明2
你提到的情况是正确的。A
不出栈的原因是递归调用还没有完成。为了更清楚地解释递归调用的过程,让我们重新逐步分析每一个递归调用和返回的过程,并使用栈来跟踪函数调用。
示例二叉树
假设有如下二叉树:
A
/ \
B C
/ \
D E
使用栈模拟执行过程
我们以栈的方式逐步模拟 draw
函数调用 draw_
的过程。
-
初始状态:
- 栈为空。
- 调用
draw(root)
,进入draw_(root, 0)
。
-
处理根节点
A
:- 调用
draw_(A, 0)
。 - 栈:[
draw_(A, 0)
]
- 调用
-
递归处理右子树
C
:- 调用
draw_(C, 1)
。 - 栈:[
draw_(A, 0)
,draw_(C, 1)
]
- 调用
-
处理
C
节点:- 右子树为空,返回。
- 打印
C
(缩进 4 空格)。 - 左子树为空,返回。
- 返回
draw_(A, 0)
。 - 栈:[
draw_(A, 0)
]
输出:
C
-
打印根节点
A
:- 打印
A
(无缩进)。 - 栈:[
draw_(A, 0)
]
输出:
C A
- 打印
-
递归处理左子树
B
:- 调用
draw_(B, 1)
。 - 栈:[
draw_(A, 0)
,draw_(B, 1)
]
- 调用
-
处理
B
节点:- 调用
draw_(E, 2)
(递归处理右子树)。 - 栈:[
draw_(A, 0)
,draw_(B, 1)
,draw_(E, 2)
]
- 调用
-
处理
E
节点:- 右子树为空,返回。
- 打印
E
(缩进 8 空格)。 - 左子树为空,返回。
- 返回
draw_(B, 1)
。 - 栈:[
draw_(A, 0)
,draw_(B, 1)
]
输出:
C A E
-
打印
B
节点:- 打印
B
(缩进 4 空格)。 - 栈:[
draw_(A, 0)
,draw_(B, 1)
]
输出:
C A E B
- 打印
-
递归处理左子树
D
:- 调用
draw_(D, 2)
。 - 栈:[
draw_(A, 0)
,draw_(B, 1)
,draw_(D, 2)
]
- 调用
-
处理
D
节点:- 右子树为空,返回。
- 打印
D
(缩进 8 空格)。 - 左子树为空,返回。
- 返回
draw_(B, 1)
。 - 栈:[
draw_(A, 0)
,draw_(B, 1)
]
输出:
C
A
E
B
D -
完成
B
节点的处理:- 返回
draw_(A, 0)
。 - 栈:[
draw_(A, 0)
]
- 返回
-
完成
A
节点的处理:- 返回
draw(root)
。 - 栈为空。
- 返回
最终输出
C
A
E
B
D
在这个过程中,A
不出栈的原因是因为它的递归调用还没有完成。只有当左子树 B
的所有处理完成后,A
才会出栈,回到 draw(root)
,表示递归完全结束。这样我们可以准确地跟踪每一步的递归调用和返回,理解栈的作用。
希望这个解释能帮助你更好地理解代码的递归逻辑!如果还有其他问题,请随时告诉我。