本次的练习是使用递归来实现单链表的各种操作,本文目录如下
目录
定义链表结点
首先要定义数据结构,定义一下构造函数方便后面写码:
struct Node
{
int data;
Node *next;
Node(int d = -1, Node *nn = NULL) : data(d), next(nn) {}
};
这里有个技巧,构造函数的参数我一般会把next结点放在后面,因为这样可以直接只传一个值data来构造新结点,next默认为NULL, 而据我的经验,直接使用一个data值构造新Node的情况比较多,同时因为设置了默认参数的缘故,也可以直接无参new Node()。
递归销毁
递归销毁和二叉树的后序遍历的思想差不多,都是先递归调用再销毁自己,这样会在递归调用到底部之后,从尾到头回溯地delete每个结点。实现代码如下:
void destroy(Node *p)
{
if (!p)
return;
destroy(p->next);
delete p;
}
顺带一提,delete一个NULL的指针是没什么问题的,C++标准有规定,这里开头需要判NULL的原因是后面需要访问p->next,如果此时p为NULL的话,p->next会直接段错误。
递归输出
void print(Node *p)
{
if (!p)
return;
cout << p->data << " ";
print(p->next);
}
与递归销毁不同的是,这里需要先输出再递归调用才是正序输出(即从头到尾),否则将会逆序输出(即从尾到头输出),除此之外,如果链表有头结点的话,需要传头结点的下一个结点进去才能正常输出,否则会一并输出头节点。
输出后不带换行。
递归尾插
这里分两种插入,一是插入到尾部,二是插入到指定顺序。遗憾的是,好像并不能统一成一个函数
尾部插入(Java写法)
Java没有指针的引用这种东西,但是C++可以用指针的引用来简化这个递归写法,所以我在这里分成两个写法。但是不管是Java写法还是C++写法,其实都是使用的C++,只不过Java写法将指针改为Java里的引用就可以变成真正的Java写法,而C++写法则是使用了一些C++的特性。
代码:
Node *TailInsertion(Node *p, int data)
{
if (!p)
return new Node(data);
else
p->next = TailInsertion(p->next, data);
return p;
}
分三个步骤理解:
- 递归的终止条件就是遇到一个传进来是NULL的指针,意味着递归到了尾结点的next位置,那么此时就是要添加结点的时候了,Java版的写法是利用返回值,此时返回一个新的结点作为插入的尾结点。
- 但是新插入的尾结点怎么挂到链表上去?这里就要看函数里的else分支了,else分支里接收递归调用的返回值复制给p->next,这样假设现在递归到最后一个结点了,p指向最后一个结点,那么else分支里递归调用传进去的p->next就是NULL(尾结点的next肯定为NULL),传进去NULL根据我们的递归终止条件,此时会返回一个新的结点,那么就刚好接收作为尾结点的next。
- 那现在不是递归到尾结点,而是中间结点,怎么保证这种修改p->next的写法不会断链表?这个时候就得看函数的底部的返回值,在处理完函数头部的if分支后直接原样得返回p,这样就会返回到上一级递归调用的else分支,而else分支里是这样写的:p->next = TailInsertion(p->next, data),对于中间结点来说,就等于p->next = p->next,当然不会断链。
TailInsertion的函数作用是传进去一个链表的头结点,返回插入尾结点后的链表,听起来比较难理解,但是当你结合这个函数的作用来理解函数里面递归调用的地方就不难理解了。可以说这种写法是一种比较 “正统” 的递归写法,因为按递归定义来说,其实这个函数符合递归的定义:链表的一部分也是一个链表,即是以结点p为首的链表是一个链表,同样以p->next结点为首的链表也同样是个链表。
不管怎么样,初学者还是要多写多练多思考才能彻底掌握递归。
尾部插入(C++写法)
利用C++里指针的引用,可以写出更加简化的代码,但是也比较难以理解,需要读者对C++指针、引用理解得很透彻才能掌握。
代码:
void TailInsertion2(Node *&p, int data)
{
if (!p)
p = new Node(data);
else
TailInsertion2(p->next, data);
}
注意,函数参数p是一个指针的引用,利用此特性可以免去返回值,直接定义成void返回值。
说说为什么可以这样写。其实这个指针的引用,就相当于是个二级指针,假设现在有个链表:
header-> 1 -> 2 -> 3 -> 4 -> 5 -> NULL
假设当前函数递归到结点3的位置,也就是函数参数p传进来的是指向结点3的指针,此时对指针p解引用会得到结点3,但是如果我们修改p,将会修改结点2的next指针,因为在函数递归到结点2的时候,在else分支将结点2的next作为参数递归调用,所以递归到结点3的时候,参数传进来的引用其实是结点2的next指针,根据引用的特性,修改它自然也就修改了上一个结点的next。按照这个规则,当函数递归到NULL的时候,p这个指针本身是结点5的next指针,只是它指向NULL而已。
这个写法确实难理解得多,还是得多写多练多思考才能掌握。
递归在指定位置插入
Java写法的实现代码:
Node *insert(Node *p, int index, int data)
{
if (!p)
return NULL;
if (index == 0)
return new Node(data, p);
else
p->next = insert(p->next, index - 1, data);
return p;
}
每次递归的时候将index减1,并判断index为0时插入,即可实现指定递归位置插入且不需要外部变量辅助。要注意的是在index为0的时候new的结点是要把p作为该结点的next传进去构造的,否则会断掉后面的链,并且这种插入方法只支持在头部和中间插入,不能在尾部插入。但是这个算法的鲁棒性还是很好的,即使你index传的是负数或是超出链表长度的数,这个函数就会在递归遍历完链表后返回,什么都不会发生。
写个main函数测试一下:
int main()
{
Node *header = new Node;
for (int i = 1; i <= 10; ++i)
TailInsertion(header, i);
cout << "origin linkedlist:" << endl;
print(header->next);
cout << endl;
cout << "mid insertion:" << endl;
insert(header, 5, 99);
print(header->next);
cout << endl;
cout << "head insertion:" << endl;
insert(header, 1, 88);
print(header->next);
cout << endl;
cout << "illeage insertion:" << endl;
insert(header, 50, 77);
print(header->next);
cout << endl;
cout << "illeage insertion2:" << endl;
insert(header, -5, 77);
print(header->next);
cout << endl;
destroy(header);
return 0;
}
输出:
origin linkedlist:
1 2 3 4 5 6 7 8 9 10
mid insertion:
1 2 3 4 99 5 6 7 8 9 10
head insertion:
88 1 2 3 4 99 5 6 7 8 9 10
illeage insertion:
88 1 2 3 4 99 5 6 7 8 9 10
illeage insertion2:
88 1 2 3 4 99 5 6 7 8 9 10
附一个C++指针引用的版本:
void insert2(Node *&p, int index, int data)
{
if (!p)
return;
if (index == 0)
p = new Node(data, p);
else
insert2(p->next, index - 1, data);
}
递归删除值为x的结点
原理和上文指定位置插入差不多,只不过new结点变成了delete结点,C++指针引用的实现:
void delNode2(Node *&p, int x)
{
if (!p)
return;
if (p->data == x)
{
Node *tmp = p->next;
delete p;
p = tmp;
}
else
delNode2(p->next, x);
}
当匹配到当前结点的值与x相同时,直接删除当前结点并返回当前结点的下一个结点。
Java版:
Node *delNode(Node *p, int x)
{
if (!p)
return NULL;
if (p->data == x)
{
Node *ret = p->next;
delete p;
return ret;
}
else
p->next = delNode(p->next, x);
return p;
}
要注意不管哪个版本,如果链表带有头结点的话一定得传头结点的下一个结点,像这样 delNode(header->next,5) ,否则头结点的值也会被判断,如果头结点的值刚好与x相同头结点就会被删掉。
本文完整代码
测试代码并没写完整,就不给出了,可以自己写点代码测试测试。
#include <iostream>
#include <algorithm>
using namespace std;
struct Node
{
int data;
Node *next;
Node(int d = -1, Node *nn = NULL) : data(d), next(nn) {}
};
void destroy(Node *p)
{
if (!p)
return;
destroy(p->next);
delete p;
}
void print(Node *p)
{
if (!p)
return;
cout << p->data << " ";
print(p->next);
}
Node *TailInsertion(Node *p, int data)
{
if (!p)
return new Node(data);
else
p->next = TailInsertion(p->next, data);
return p;
}
void TailInsertion2(Node *&p, int data)
{
if (!p)
p = new Node(data);
else
TailInsertion2(p->next, data);
}
Node *insert(Node *p, int index, int data)
{
if (!p)
return NULL;
if (index == 0)
return new Node(data, p);
else
p->next = insert(p->next, index - 1, data);
return p;
}
void insert2(Node *&p, int index, int data)
{
if (!p)
return;
if (index == 0)
p = new Node(data, p);
else
insert2(p->next, index - 1, data);
}
void delNode2(Node *&p, int x)
{
if (!p)
return;
if (p->data == x)
{
Node *tmp = p->next;
delete p;
p = tmp;
}
else
delNode2(p->next, x);
}
Node *delNode(Node *p, int x)
{
if (!p)
return NULL;
if (p->data == x)
{
Node *ret = p->next;
delete p;
return ret;
}
else
p->next = delNode(p->next, x);
return p;
}
int main()
{
Node *header = new Node;
for (int i = 1; i <= 10; ++i)
TailInsertion(header, i);
cout << "origin linkedlist:" << endl;
print(header->next);
cout << endl;
//test here..
destroy(header);
return 0;
}
结束语
递归确实是一个比较难的内容,特别是利用指针引用这种思路更是难以理解,但是只要我们多写多练多思考,慢慢地总会熟练掌握的,共勉。
1478

被折叠的 条评论
为什么被折叠?



