最近在学习《c和指针》这本书,感觉书中有很多的编程技巧,之前在学习过程中,没有学习到,这里做个简单的记录。
对于c语言的链表,简单来说,可以有单链表和双链表两种,每种都有实际运用中的优势所在。但是链表因为操作的是指针,因此也存在更大的风险。对于链表与数组的优劣不是本文的重点,本文主要是想以链表的插入函数举例,讲解对于代码的部分优化、简化,完成代码实现方式的改变和减小代码体积。
下面是未优化的单链表的插入函数的实现方式:
#include <stdio.h>
#include <stdlib.h>
#define FAILE 0
#define TRUE 1
typedef struct NODE{
struct NODE *link;
int value;
}Node;
int sll_insert(Node **rootp, int new_value)
{
Node *current;
Node *previous;
Node *new_node;
current = *rootp;
previous = NULL;
/*寻找正确的插入的位置,方法是按序访问链表*/
while(current != NULL && current->value < new_value)
{
previous = current;
current = current->link;
}
/*为新节点分配内存*/
new_node = (Node *)malloc(sizeof(Node));
if (new_node == NULL)
{
return FAILE;
}
new_node->value = new_value;
/*把新节点插入链表中*/
new_node->link = current;
if (previous == NULL)
{
*rootp = new_node;
}
else
{
previous->link = new_node
}
return TRUE;
}
单链表的操作是单向性的,对于插入操作,要正确的插入一个节点,就需要保存上一个节点的指针。但是这也就存在一个问题了,如果插入的位置是首个节点,就需要判断上一个节点previous是不是NULL,从而得知该插入的位置是首个节点。但是这部分实现能不能被优化,即不用理会该插入的位置是不是首个节点,答案是肯定的。
消除这个特殊情况的关键在于:必须认识到链表中的每个节点都有一个指向它的指针。对于首个节点,指向它的指针就是根指针,而对于其他节点,这个指针就是前一个节点的link字段。而且对于c语言来说,允许读取指针的地址,也就是操作符&,其他语言可能不允许有这样的操作存在。
下面给出实现优化后的单链表的代码:
#include <stdio.h>
#include <stdlib.h>
#define FAILE 0
#define TRUE 1
typedef struct NODE{
struct NODE *link;
int value;
}Node;
int sll_insert(register Node **linkp, int new_value)
{
register Node *current;
register Node *new_node;
/*寻找正确的插入的位置,方法是按序访问链表*/
while((current = *linkp) != NULL && current->value < new_value)
{
linkp = ¤t->link;
}
/*为新节点分配内存*/
new_node = (Node *)malloc(sizeof(Node));
if (new_node == NULL)
{
return FAILE;
}
new_node->value = new_value;
/*把新节点插入链表中*/
new_node->link = current;
*linkp = new_node;
return TRUE;
}
而对于双链表,相比於单链表,双链表可以实现以任何方向遍历链表。对于在一条双链表中插入一个节点,需要考虑的情况有4种:
1、新值可能必须插入到链表的中间位置;
2、新值可能必须插入到链表的起始位置;
3、新值可能必须插入到链表的结束位置;
4、新值可能必须即插入到起始位置,又插入到结束位置(即原链表为空);
在实际编程中当然可以针对这4种情况进行代码实现。下面给出实现的代码:
#include <stdio.h>
#include <stdlib.h>
typedef struct NODE{
struct NODE *fwd;
struct NODE *bwd;
int value;
}Node;
int dll_insert(Node *rootp, int value)
{
Node *prev;
Node *next;
Node *newnode;
/*prev将指向应该在新节点之前的那个节点,next将指向应该在新节点之后的那个节点*/
for(prev = rootp; (next = prev->fwd) != NULL; prev = next)
{
if (next->value == value)
{
return 0;
}
if (next->value > value)
{
break;
}
}
newnode = (Node *)malloc(sizeof(Node));
if (newnode == NULL)
{
return -1;
}
newnode->value = value;
if (next != NULL)
{
/*情况1或2:并非位于链表尾部*/
if (prev != rootp)
{
/*情况1:并非位于链表起始位置*/
newnode->fwd = next;
prev->fwd = newnode;
newnode->bwd = prev;
next->bwd = newnode;
}
else
{
/*情况2:位于链表起始位置*/
newnode->fwd = next;
rootp->fwd = newnode;
newnode->bwd = NULL;
next->bwd = newnode;
}
}
else
{
/*情况3或4:位于链表的尾部*/
if (prev != rootp)
{
/*情况3:并非位于链表的起始位置*/
newnode->fwd = NULL;
prev->fwd = newnode;
newnode->bwd = prev;
prev->bwd = newnode;
}
else
{
/*情况4:位于链表的起始位置*/
newnode->fwd = NULL;
rootp->fwd = newnode;
newnode->bwd = NULL;
rootp->bwd = newnode;
}
}
return 1;
}
仔细查看代码,可以发现在各个嵌套的if语句中存在相似之处。因此就可以对其进行提炼、简化。这里将会依次用到两个技巧。
第1个技巧:
if (x == 3)
{
i = 1;
do_something;
j = 2;
}
else
{
i = 1;
do_something_different;
j = 2;
}
转换等价于:
i = 1;
if (x == 3)
{
do_something;
}
else
{
do_something_different;
}
j = 2;
第2个技巧:
if (pointer != NULL)
{
field = pointer;
}
else
{
field = NULL;
}
等价于:
field = pointer;
对于这两个技巧的运用,关键在于把看上去不一样但实际上执行了相同任务的语句进行改写,写成同一种形式。下面给出最终的简化结果:
#include <stdio.h>
#include <stdlib.h>
typedef struct NODE{
struct NODE *fwd;
struct NODE *bwd;
int value;
}Node;
int dll_insert(Node *rootp, int value)
{
Node *prev;
Node *next;
Node *newnode;
/*prev将指向应该在新节点之前的那个节点,next将指向应该在新节点之后的那个节点*/
for(prev = rootp; (next = prev->fwd) != NULL; prev = next)
{
if (next->value == value)
{
return 0;
}
if (next->value > value)
{
break;
}
}
newnode = (Node *)malloc(sizeof(Node));
if (newnode == NULL)
{
return -1;
}
newnode->value = value;
newnode->fwd = next;
prev->fwd = newnode;
if (prev != rootp)
{
newnode->bwd = prev;
}
else
{
newnode->bwd = NULL;
}
if (next != NULL)
{
next->bwd = newnode;
}
else
{
rootp->bwd = newnode;
}
return 1;
}
问题来了,上面的代码能不能继续简化,答案是可以的,比如:
/*把节点添加到链表中*/
newnode->fwd = next;
prev->fwd = newnode;
newnode->bwd = prev != rootp ? prev :NULL;
(next != NULL ? next : rootp)->bwd = newnode;
总结:
对于代码的优化和简化在一定程序上可以提高程序的运行速度和减小程序的体积,但是另一方面,优化或简化代码会降低程序的可读性,难于维护。因此对于一个程序员来说,权衡两者的利弊就是其该做的事。致力于编写高效且易维护的程序才是一名程序员要追求的方向之一。