您的位置:首页 > 其它

单向链表的基本操作

2017-08-02 16:53 155 查看

一、链表与数组:

  首先,我们来看看为啥我们有了数组,还需要链表呢?那么我们来看看链表与数组的区别。

二者都属于一种数据结构:

从逻辑结构来看:

1、 数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费;数组可以根据下标直接存取。

2、 链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项,非常繁琐)链表必须根据next指针找到下一个元素

从内存存储来看:

(静态)数组从栈中分配空间, 对于程序员方便快速,但是自由度小

链表从堆中分配空间, 自由度大但是申请管理比较麻烦

  从上面的比较可以看出,如果需要快速访问数据,很少或不插入和删除元素,就应该用数组;相反, 如果需要经常插入和删除元素就需要用链表数据结构了。

二、单向链表的相关操作:

1、定义链表结构体:

这里没有使用头结点。

struct Node
{
int num;              //这里用来方便查找链表位置,可以省略
datatype data;        //数据
struct Node *pNext;   //指向下一个结点
};
typedef struct Node node;   //定义一个node类型


2、插入数据(后面):

void backadd(node **ppnode,int num, datatype data)
{
node *pNewnode = (node *)malloc (sizeof(node));

//creat a  new node
pNewnode->num =num;    //给新结点赋值
pNewnode->data=data;

if(*ppnode == NULL)     //如果首元结点为空
{
*ppnode = pNewnode;     //将新加入的结点作为首元结点
pNewnode->pNext = NULL;
}

else  //the first node isn't empty
{
node *p = *ppnode;    //head

while(p->pNext != NULL)
{
p=p->pNext;
}
p->pNext = pNewnode;   //Insert

}

}


这里用到了二级指针,用于保存头结点指针的地址。如果不用二级指针则需要一个node *的返回值,才能改变结点的地址。

调用:

node *pNode = NULL;    //定义头结点
backadd(&pNode,1,11);   //出入的是地址


2、结点查找:

  因为我们在结构体中每个结点我们能设置了一个num,现在我们就可以通过匹配这个num来查找某个结点。

node *search(node *pNode,int num)
{
node *p=pNode;
for(;p!=NULL; p=p->pNext)  //遍历整个链表,知道为空
{
if(num == p->num)   //通过设定数字查找
{
return p;
break;
}
}
return NULL;
}


3、替换结点内容:

  这里与查找有些类似,需要先查找到对于结点,然后再对其内容进行替换。

int change(node *pNode,int oldnum,int newnum,datatype newdata)
{
while(pNode->pNext != NULL) //终止条件
{
if(pNode != NULL)
{
if(oldnum == pNode->num)
{
pNode->num=newnum;  //赋值
pNode->data=newdata;
return 1;
}
pNode = pNode->pNext; //遍历整个链表,一直到最后pNode->pNext = NULL
}
}

printf("can't find the node,modify failed!");
return 0;
}


4、删除结点

让上一个结点直接跳过要删除的结点,指向要删除结点的下一个结点。



node *delete(node *pNode,int num)
{
node *p1 = NULL;
node *p2 = NULL;

p1=pNode;   //Save the infomation of the linklist

while(p1 != NULL)
{
if(p1->num == num)   //查找结点
{
break;
}
else
{
//p2指向前一个结点
p2=p1;  //Save the previous node
p1=p1->pNext;
}
}

if(p1 == pNode)   //要删除的点是第一个结点
{
pNode=p1->pNext;   //把第一个结点地址变为下一个结点地址
free(p1);
}

else
{
p2->pNext=p1->pNext;  //ignore p1(delete it)
free(p1);
}
return pNode;
}


这里也可以用for循环来实现,传入一个n值来控制要删除具体那个结点。用for循环控制p要移动的次数。接下来,我们在数据的插入中将会用到。

5、数据插入(中间,前面)

  和删除类似,需要找到前后两个结点并用p1,p2来保存。再进行相关操作。是删除结点的反操作。

在中间插入图解:



void insert(node **pNode,int n,int newnum,datatype newdata)
{
node *p1,*p2;
p1=p2=NULL;
p1=*pNode;
int i=0;

while(p1 != NULL)
{
for(i=1;i<n;i++)   //用for循环找出要在第几个结点前面插入
{
p2=p1;
p1=p1->pNext;
}
break;
}

node *pNewnode=(node *)malloc(sizeof(node));
pNewnode->num=newnum;
pNewnode->data=newdata;
printf("%p",pNewnode);

if(pNode == p1)    //insert it on the head
{
pNewnode->pNext = *pNode;
*pNode=pNewnode;
}

else            //中间
{
pNewnode->pNext=p1;
p2->pNext=pNewnode;
}

}


6、链表逆序:

  逆序:数据颠倒,但是地址不变。也就是说要把链表的指向颠倒过来。需要三个变量,p1,p2用来遍历整个链表,p3类似于一个临时变量,方便p1,p2进行数据交换。最后p2==NULL时退出,p1地址作为头结点的值,并且把原来头结点的下一个地址置空。



node *inverted(node *pNode)
{
node *p1,*p2,*p3;
p1=p2=p3=NULL;
p1=pNode;
if(pNode == NULL || pNode->pNext ==NULL)
{
return pNode;
}
else
{
p1=pNode;
p2=pNode->pNext;
}

while(p2 != NULL)
{
p3 = p2->pNext;
p2->pNext=p1;
p1=p2;
p2=p3;
}
pNode->pNext = NULL;  //首元结点变最后一个结点,所以指向空
pNode=p1;             //p1移动到最后,把p1地址赋给首元结点。
return pNode;
}


7、链表摧毁:

移动一次后,把前面的结点free并置空。

void link_destory(node *pNode)
{
node *p1;
while( pNode != NULL)
{
p1=pNode->pNext;
pNode=p1;
free(p1);
p1=NULL;
}
}


总结:

关于free()与野指针

  链表的操作关键在与操作指针,所以经常出现段错误。为了避免这种情况,我们定义指针后一定要对它进行初始化,而且操作之前要判断是否为NULL,这很重要,经常容易出错。还有就是,在用完要记得free,那么之后,free之后,指针就变成了野指针,一定要把指针赋值为NULL。

比如,在这个例子中,我们可以看到,指针free之后地址依旧不变,只不过它的值变了,变成了一个野指针。



形象点来说就是:

你大学的宿舍,入学后就申请,大学四年一直是你在用,别人不能用;大学毕业后,还给了学校,学校究竟有没有分配给别人你就不清楚了,反正你再也不能使用; 分配给你的寝室号一直在那,你的心也一直记得那个号码

关于二级指针

  指针的指针就是二级指针。指针用来存放地址,那么二级指针就是用来存放指针的地址。所以当程序中操作链表结点的地址时候,比如,要更改头结点的时候,就需要通过二级指针来实现,我们传入的头结点是一个,node**类型这时候不需要返回值。我们如果不想用二级指针,也可以通过通过返回 node * 的头结点来实现。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息