2.2 Implement an algorithm to find the nth to last element of a singly linked list.
译文:实现一个算法返回单链表中倒数第n个元素
第一反应是递归,但还是分析一下,单链表返回第n个元素很好办,直接从头结点开始遍历n个。这里返回倒数第n个元素,我们总不能先得出这个链表的元素个数N,然后遍历第N-n+1个吧。这种次序颠倒的问题我们可以想到另一种数据结构——栈,栈有个特点就是先进后出,我们可以遍历一遍链表,将其中的元素逐个压栈,然后再将各个元素弹出来,那么第n个弹出来的元素就是返回值。如果要是再显式地写一个栈来实现的话,就复杂化了,看到栈,我们就得联想到递归,这个自动使用栈的方式,所以我们解决这个问题的第一个方法便是使用递归。
思路比较简单,就是不断遍历,不断“压栈”,遍历完后,就“出栈”,n不断减1,减为0时,此时弹出的元素就是倒数第n个元素。代码如下
LINK_NODE *Node = NULL;
int n;
void findNthToLast(LINK_NODE *pHead)
{
if (NULL == pHead)
return;
findNthToLast(pHead->next);
--n;
if (0 == n)
Node = pHead;
}
递归有时让人摸不着头脑,因为我们有人是通过分析堆栈,分析一个一个函数的调用过程和输出结果来分析递归算法的,简单的还好,要是多几级,这种方式只会把自己弄晕。其实递归本质上也是函数的调用,调用自己的函数和其他函数都是差不多的,没本质区别,函数调用总会将一些临时信息(主要是函数参数和返回值)压栈保存,压栈只是为了函数能够正确的返回,我们使用递归时,我们感兴趣的信息变量(递归时一般就是函数形参和返回值)就在不断地压栈出栈,所以我们在理解递归的时候可结合栈这一数据结构来分析。递归运行时可以概括为“能进则进,不进则退”,进的时候压栈,进不了我就退,就不断的出栈。所以递归的效率一直被人诟病,但我们不能忘记它的好,可正常使用的递归必须要有终止条件,不能无限制的递归下去,不然最后就是栈溢出了。
除了递归,这里还提供另一个方法,要返回单链表中倒数第n个元素,我们可以借用两个指向链表节点的指针,并使其间隔距离为n-1,将这两个指针同步向链表尾移动,当前面那个移动到尾部时,那么后面那个就是指向倒数第n个链表节点。代码如下
LINK_NODE* findNthToLast(LINK_NODE *pHead, int n)
{
if ((NULL == pHead) || (n < 1))
return NULL;
LINK_NODE *ptr_front = pHead, *ptr_behind = pHead;
for (int i = 0; i < n; ++i)
{
if (NULL == ptr_front)
return NULL; //表明链表的长度小于n
ptr_front = ptr_front->next;
}
while (ptr_front != NULL)
{
ptr_front = ptr_front->next;
ptr_behind = ptr_behind->next;
}
return ptr_behind;
}
2.3 Implement an algorithm to delete a node in the middle of a single linked list, given only access to that node.
EXAMPLE Input: the node ‘c’ from the linked list a->b->c->d->e Result: nothing is returned, but the new linked list looks likea->b->d->e
译文:实现一个算法来删除单链表的中间结点,只给出指向那个结点的指针
例子 输入:指向链表 a->b->c->d->e 中结点c的指针 结果:无返回值,得到一个新链表 a->b->d->e
这里要清楚的是题目只给出一个指向要删除结点的指针,并没有给出整个链表的头结点指针,所以我们不能直接删除这个结点,否则链表就断了。这是单链表,我们也不能定位被删结点的前面那个结点,所以我们只能从该结点后面的结点做文章,我们只能遍历到被删结点之后的结点。对于链表 a->b->c->d->e,我们仅知道指向c结点的指针,要删除c之后的结点很简单,无论是d还是e,比如删除d,只需node *t = c->next; c->next = t->next; delete t;即可,得到的链表就是 a->b->c->e,与我们预期要得到的链表 a->b->d->e,仅相差一个结点,我们平时基本上都是通过修改结点里面的指针成员指向来修改链表,其实修改里面的数据成员也能达到修改链表的效果,结点结构都是一样的,重要是结点里面的数据。这里我们在上面的基础上就通过修改结点c中的数据成员,将d中的数据传给c就可以了。
bool deleteNode(LINK_NODE *c)
{
if ((NULL == c) || (NULL == c->next))
return false; //没有考虑被删结点为尾结点的情况
LINK_NODE *t = c->next;
c->data = t->data;
c->next = t->next;
delete t;
return true;
}
这是利用被删除结点后面的那个结点来进行处理的,那如果被删除的结点恰是最后一个尾结点呢,由于其后面没有结点,我们就不能采用这种方法来进行处理。要是直接删除尾结点c会怎样勒,我们要知道 delete c 或 free (c) 并不是真正意义的完全删除那个结点(其他也是一样),只是一个”解绑的关系“,就是解除c 这个指针与它指向的内存地址数据之间的关系,一旦解除了,c 对它所指向的内存就没有使用权了,这块内存就可以被别人使用了(不是说别人可以用里面的数据),这就是内存释放,不过这个内存地址里面的数据还是原来c关联的数据,但跟c没有半毛钱关系了,当然其实跟谁也没有关系,因为谁也找不到(没有指针与它相关联,你申明一个变量它也会有地址,世界是物质的),只有重新分配(系统和手动)的时候,用到了这个内存的时候,这个内存就可以重新使用了。delete c之后 c 本身还是原来的数据,即c还是指向原来的内存地址单元,但是不能访问这个地址单元里面的数据了,这个指针没有关联任何数据,这就是”野指针“,所以在删除指针的时候,通常要将其设置为NULL,这样主要是为在校验的时候有作用。上面嘀咕了一大堆,不知道啰嗦清楚了没。
所以上面直接删除c,然后在打印的时候会出错(我调试运行的时候会出错,原因上面说了)。这里再针对上面不能删除尾结点的情况进行修改,先贴代码
bool deleteNode(LINK_NODE **pNode)
{
if ((NULL == *pNode))
return false;
if (NULL == (*pNode)->next)
{
delete (*pNode);
*pNode = NULL;
return true;
}
LINK_NODE *t = (*pNode)->next;
(*pNode)->data = t->data;
(*pNode)->next = t->next;
delete t;
return true;
}
函数形参是对其实参进行了一份拷贝,内容一样,但是地址不一样,所以需要指针来处理,本身是指针,就是指针的指针了。这里根据题目情况适当进行了修改,在主程序中在这样申明调用即可
LINK_NODE **pNode = &c ;
//这里c为给定的链表中要被删除的结点
if (deleteNode(pNode))
{
PrintLinkNode(pLinkNode);
}
else
cout << "failure" << endl;
既然用了双指针,那么也可用指针的引用来实现
bool deleteNode(LINK_NODE *&pNode)
{
if ((NULL == pNode))
return false;
if (NULL == pNode->next)
{
delete (pNode);
pNode = NULL;
return true;
}
LINK_NODE *t = pNode->next;
pNode->data = t->data;
pNode->next = t->next;
delete t;
return true;
}
注意的是,在主程序中调用的时候,直接用给定的c结点作为参数传入,不要通过声明另一个变量传入,不然引用的就不是c了。