摘自《C和指针》中关于单链表的描述
在单链表中,每个节点包含一个指向链表下一个节点的指针。链表最后一个节点的指针字段的值为NULL,提示链表后面不再有其它节点。在你找到链表的第一个节点后,指针就可以带你访问剩下的所有节点。为了记住链表的起始位置,可以使用一个根指针(root pointer)。根指针指向链表的第一个节点。注意根指针只是一个指针,它不包含任何数据。
下面是一张单链表的图:
假设链表是以升序进行排练的。首先定义链表的数据结构和一个打印链表的辅助函数。
sll_node.h
#ifndef _SLL_NODE
#define _SLL_NODE
typedef struct NODE{
struct NODE *link;
int value;
}Node;
#endif
sll_node.c
#include "sll_node.h"
#include <stdio.h>
#include <stdlib.h>
void printSll(Node * root){
while(root!=NULL){
printf("%d ",root->value);
root=root->link;
}
}
然后是插入函数的编写:从链表的起始位置开始,跟随指针直到直到第一个大于插入值的节点,然后把这个新值插入到那个节点之前的位置。
下面是insert1.c的代码
#include <stdlib.h>
#include <stdio.h>
#include "sll_node.h"
#define FALSE 0
#define TRUE 1
/*插入到一个有序的单链表,函数的参数是一个指向链表的第一个节点的指针以及需要插入的值*/
int sll_insert(Node *current,int new_value){
Node * previous;
Node *new;
/*寻找正确的插入值,方法是按顺序访问链表,直到到达其值大于或等于新插入值的节点*/
while(current->value<new_value){
previous=current;
current=current->link;
}
/*为新节点分配内存,并把新值存储到新节点中,如果内存分配失败,函数返回FALSE*/
new=(Node *)malloc(sizeof(Node));
if(new==NULL)
return FALSE;
new->value=new_value;
/*把新节点插入到链表中,并返回TRUE*/
new->link=current;
previous->link=new;
return TRUE;
}
int main(){
Node * root;
Node first,second,third;
first.value=5;
second.value=10;
third.value=15;
root=&first;
first.link=&second;
second.link=&third;
third.link=NULL;
int result=sll_insert(root,3);
printSll(root);
}
这段代码在插入大于5小于15的数值时不会有问题,但是插入小于5或者大于15的数字时会出错,表现就是程序直接崩溃。因为这段代码的逻辑对于插入的节点在根节点之前或者在尾节点之后这两种情况的处理并不正确。
插入值大于15时,在while循环内会出错,此时current为NULL,对它进行解引用操作会失败,修改方法是将while循环改成:
while(current!=NULL&¤t->value<new_value)
当插入值小于5时,此次while循环会直接跳出,一次都不执行。此时就只剩下这两行代码修改节点之前的关系:
new->link=current;
previous->link=new;
注意到这两行代码其实都是无效的了,第一行代码虽然把新插入的几点指向了根节点,但是它并没有修改调用函数外面的根节点的指针,因为此时它是根节点了,第二行代码就更错了,此时previous还没有初始化。
它的解决办法就是把一个指向root节点的指针作为参数传递给函数。然后,使用解引用,函数不仅可以获得root(指向链表第一个节点的指针,也就是根指针)的值,还可以向它存储一个新的指针值。
下面是修改后的代码:
insert2.c
#include <stdio.h>
#include <stdlib.h>
#include "sll_node.h"
#define FALSE 0;
#define TRUE 1
/*插入到一个有序的单链表,函数的参数是一个指向链表根指针的指针,以及一个需要插入的新值*/
int sll_insert(Node **rootp,int new_value){
Node *current;
Node *previous;
Node *new;
/* 得到指向第一个节点的指针*/
current=*rootp;
previous=NULL;
/*寻找正确的插入位置,方法是按序访问链表,知道到达一个大于或等于新值的节点*/
while(current!=NULL&¤t->value<new_value){
previous=current;
current=current->link;
}
/*为新节点分配内存,并把新值存储到新节点中,如果内存分配失败,函数返回FALSE*/
new=(Node *)malloc(sizeof(Node));
if(new==NULL){
return FALSE;
}
new->value=new_value;
/*把新节点插入到链表中,并返回TRUE*/
new->link=current;
if(previous==NULL)
*rootp=new;
else
previous->link=new;
return TRUE;
}
int main(){
Node * root;
Node first,second,third;
first.value=5;
second.value=10;
third.value=15;
root=&first;
first.link=&second;
second.link=&third;
third.link=NULL;
sll_insert(&root,3);
printSll(root);
return 0;
}
上面的代码中main函数中链表的创建和insert1.c中链表的创建是一样的,但是传递给函数的参数在insert1.c是root,也就是链表中第一个节点的地址,在insert2.c中传递给函数的地址是&root,就是这个地址的地址,理解了C中函数是传值调用这个观点后,很容易明白传递&root的好处,就是可以在函数内修改root的内容,使root始终指向链表的第一个节点。
对上面的main.c稍作修改,打印出插入几个值后root变量的值看看就很容易理解了:
int main(){
Node * root;
Node first,second,third;
first.value=5;
second.value=10;
third.value=15;
root=&first;
first.link=&second;
second.link=&third;
third.link=NULL;
printf("address of root:%p\n",&root);
printf("init value of root:%p\n",root);
printSll(root);
printf("\n");
sll_insert(&root,3);
printf("address of root:%p\n",&root);
printf("insert 3:value of root:%p\n",root);
printSll(root);
printf("\n");
sll_insert(&root,7);
printf("address of root:%p\n",&root);
printf("insert 7:value of root:%p\n",root);
printSll(root);
printf("\n");
sll_insert(&root,20);
printf("address of root:%p\n",&root);
printf("insert 20:value of root:%p\n",root);
printSll(root);
return 0;
}
上面往创建的链表里分别依次插入了3,7,20,这三个数,创建完链表或者每次插入一个新的数据都都打印出root变量的地址(也就是传递给sll_insert函数的参数),root变量的值,还有链表的数据。在输出结果之前可以先猜想,四次输出中&root的值应该是不变的,因为传递给函数的只是实参的副本(c中传值调用),第一次输出的root是一个值,第二次输出的root和第一次不同,因为插入了3,此时值为3的那个节点才是根节点,第三次和第四次输出的root值和第二次相同,因为插入7和插入20不会更改指向根节点的指针的值。
下面是输出的结果:与上面的猜想是一致的。
上面的代码其实已经是一种正确的处理方法了,但是其实还有一种更优的方法。上面的代码把第一个节点插入到链表的起始位置作为一种特殊的情况在处理,因为此时修改的是根指针。对于任何其它节点,对指针的修改实际上修改的是前一个节点的link字段。这两个看上去不同的操作实际上是一样的。
消除特殊情况的关键在于:必须认识到,链表中的每一个节点都有一个指向它的指针。对于第一个节点,这个指针是根指针;对于其它节点,这个指针是前一个节点的link字段。重点在于每个节点都有一个指针指向它。至于该指针是不是位于一个节点内部则无关紧要。
再次观察这个链表,为了弄清这个概念。这是第一个节点和指向它的指针。
如果新值插入到第一个节点之前,这个指针就必须进行修改。
下面是2个节点和指向它的指针:
如果新值需要插入到第2个节点之前,那么这个指针必须进行修改。注意我们只考虑指向这个节点的指针,至于那个节点包含这个指针则无关紧要。对于链表中的其它节点,都可以应用这个模式。
现在看看修改之后的函数(当它开始执行时)。羡慕显示了第1条赋值语句之后各个变量的情况:
我们拥有一个指向当前节点的指针(即current指针),以及一个“指向当前节点的link字段的”指针(即rootp指针),后面半句话感觉有点歧义,意思就是指向当前节点的指针的指针,从图中很容易看出它的含义。除此之外,我们就不需要别的了!如果当前节点的值大于新值,那么rootp指针就会告诉我们那个link字段必须进行修改,以便让新节点链接到链表中。如果在链表其它位置的插入也可以用同样的方式进行表示,就不存在前面提到的特殊情况了。其关键在于我们看到的指针/节点关系。
当移动到下一个节点时,我们保存一个“指向”下一个节点的link字段的“指针,而不是保存一个指向前一个节点的指针。很容易画出描述这种情况的图:
注意,这里rootp并不是指向节点本身,而是指向节点内部的link字段。这是简化插入函数的关键所在,但我们必须能够取得当前节点的link字段的地址,使用¤t->link就可以达到目的。rootp参数现在称为linkp,因为它现在指向的是不同的link字段,而不仅仅是根指针。我们不再需要previous指针,因为linkp指针可以负责找寻需要修改的link字段。insert2.c中用于处理特殊情况的代码也不见了,因为我们始终拥有一个指向需要修改的link字段的指针,我们用一种和修改节点的link字段完全相同的方式修改root变量。
下面是执行代码:
insert3.c
#include <stdio.h>
#include <stdlib.h>
#include "sll_node.h"
#define FALSE 0
#define TRUE 1
int sll_insert(register Node ** linkp,int new_value){
register Node *current;
register Node *new;
/*寻找正确的插入位置,方法是按序访问链表,知道到达一个其值大于或等于新值的节点*/
while((current=*linkp)!=NULL&¤t->value<new_value){
linkp=¤t->link;
}
/*为新节点分配内存,并把新值存储到新节点中,如果内存分配失败,函数返回FALSE*/
new=(Node *)malloc(sizeof(Node));
if(new==NULL)
return FALSE;
new->value=new_value;
/*在链表中插入新节点,并返回TRUE*/
new->link=current;
*linkp=new;
return TRUE;
}
int main(){
Node * root;
Node first,second,third;
first.value=5;
second.value=10;
third.value=18;
root=&first;
first.link=&second;
second.link=&third;
third.link=NULL;
sll_insert(&root,3);
printf("root address3:%p\n",root);
sll_insert(&root,1);
printf("root address1:%p\n",root);
printSll(root);
return 0;
}
下面是添加的查找和删除的函数:
/*给定一个值,返回value为该值的节点的指针,如果没有找到,返回NULL*/
Node * findNode(Node * root,int value){
while(root!=NULL&&root->value<value){
root=root->link;
}
if(root!=NULL&&root->value==value)
return root;
else
return NULL;
}
/*根据给定的值删除对应的节点,删除成功返回1,没有找到对于的节点返回0*/
int removeNode(Node **linkp,int value){
Node *current;
while((current=*linkp)!=NULL&¤t->value!=value){
linkp=¤t->link;
}
/*跳出循环可能是因为current为NULL或者找到了正确的值,需要进行判断*/
if(current!=NULL&¤t->value==value){
*linkp=current->link;
return TRUE;
}else{
return FALSE;
}
}
偶尔练手写的几个链表的建立,删除,插入,反转,排序的函数,边界值都测试过没有问题:
#include <stdio.h>
#include <stdlib.h>
typedef struct _node{
int data;
struct _node *next;
}Node;
Node * createList();//创建链表
Node * insertList(Node * head,int data);//向链表中插入元素
void printList(Node * head);//打印链表
Node * removeList(Node * head,int data);//删除链表中的元素
void orderList(Node * head);//链表排序
int lengthList(Node * head);//求链表长度
Node * reverseList(Node *head);//反转链表
Node * createList(){
Node * head=(Node *)malloc(sizeof(Node));
if(NULL==head) return NULL;
printf("input a number:");
int input;
Node * n=head;
scanf("%d",&input);
while(input!=0){
Node * nn=(Node *)malloc(sizeof(Node));
if(NULL==nn) return NULL;
nn->data=input;
n->next=nn;
n=nn;
printf("input a number:");
scanf("%d",&input);
}
n->next=NULL;
head=head->next;
return head;
}
void printList(Node * head){
while(head!=NULL){
printf("%d ",head->data);
head=head->next;
}
printf("\n");
}
Node * insertList(Node * head,int data){
if(head==NULL) return NULL;
Node * p1=head,*p2=NULL;
while(p1!=NULL&&p1->data<data){//注意这里&&的先后顺序,不能把p1!=NULL放在后面,否则会出错
p2=p1;
p1=p1->next;
}
Node * n=(Node *)malloc(sizeof(Node));
if(n==NULL) return NULL;
n->data=data;
if(p1==NULL){//插入的元素位于最尾端
p2->next=n;
n->next=NULL;
}
else if(p2==NULL)//插入首端
{
n->next=p1;
head=n;
}
else if(p1->data>data)//插入元素位于中间
{
p2->next=n;
n->next=p1;
}
return head;
}
Node * removeList(Node * head,int data){
Node * p1=head,*p2=NULL;
while(p1!=NULL&&p1->data!=data){
p2=p1;
p1=p1->next;
}
if(p1==NULL){//如果p1为空,表示没有找到需要的元素,直接返回
return head;
}
if(p1->data==data){
if(p2==NULL){//删除首节点
head=head->next;
free(p1);
}
else if(p1->next==NULL)//删除尾节点
{
p2->next=NULL;
free(p1);
}
else//删除中间节点
{
p2->next=p1->next;
free(p1);
}
}
return head;
}
void orderList(Node * head){
int i,j,len;
len=lengthList(head);
for(i=1;i<len;i++){
Node * p=head;
for(j=0;j<len-i;j++){
if(p->data > p->next->data){
int temp=p->data;
p->data=p->next->data;
p->next->data=temp;
}
p=p->next;
}
}
}
int lengthList(Node * head){
int count=0;
while(head!=NULL){
count++;
head=head->next;
}
return count;
}
/*设置三个指针p1,p2,p3,初始时p1指向头节点,p2指向第2个节点,首先将p2的下一个节点保存到p3,然后设置p2
* 的下一个节点为1,设置完成后根据保存的p3的值更新p1,p2(使它们都指向下一个节点)
* */
Node * reverseList(Node *head){
if(head==NULL||head->next==NULL) return head;
Node *p1,*p2,*p3;
p1=head;
p2=head->next;
while(p2){
p3=p2->next;
p2->next=p1;
p1=p2;
p2=p3;
}
head->next=NULL;
return p1;
}
int main(){
Node * head=createList();
printList(head);
head=reverseList(head);
printList(head);
orderList(head);
printList(head);
int input;
printf("insert number:");
scanf("%d",&input);
while(input!=0){
head=insertList(head,input);
printList(head);
printf("insert number:");
scanf("%d",&input);
}
printf("delete number:");
scanf("%d",&input);
while(input!=0){
head=removeList(head,input);
printList(head);
printf("delete number:");
scanf("%d",&input);
}
return 0;
}