单向链表的基本操作

一、链表与数组:

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

二者都属于一种数据结构:
从逻辑结构来看:

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 * 的头结点来实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值