链表的基本操作

链表

(以下内容均为单向链表)
链表是用链节指针链在一起的自引用结构变量(称为结点)的线性集合,是线性表的一种存储结构。假设我们想随心所欲创建一个结构,输入多少次数据他就有多大,可以说是游刃有余。相比数组和动态分配,这两者其实都有缺陷:数组,比如一个字符串数组,要存储书名,在我不知道所有名字到底多长时,字符串长度只能设置为最长的哪那个,浪费内存;动态分配内存,恐怕就是不断在malloc和realloc里,写起来难以理解不方便。所以介绍链表思想。
链表的性质:
(1)headPtr──指向链表首元结点的指针变量。(有的时候会有)
(2)每个结点由2个域组成:
数据域──存储结点本身的信息。
指针域──存储指向后继结点的指针(针对单向链表)。
(3)尾结点的指针域置为NULL(用反斜杠表示),作为链表结束的标志。

链表的特点:
1. 链表是一种存储结构,用于存放线性表;
2. 链表的结点是根据需要调用动态内存分配函数进行分配的,因此链表可随需要伸长缩短,在要存储的数据个数未知的情况下节省内存;
3. 链表的结点在逻辑上是连续的,但是各结点的内存通常是不连续的,因此不能立即被访问到,只能从首元结点开始逐结点访问。

如何访问链表中的结点:
由于链表中结点的内存是动态分配的,无法通过名称去访问,因此只能通过结点的地址去访问。

某结点的地址记录在其前驱结点的地址域里,因此要想访问第n个结点,必须先得访问第n-1个结点,读取该结点的地址域;而要想访问第n-1个结点,必须先得访问第n-2个结点;以此类推,一直推到访问第1个结点。而第1个结点是由指针headPtr指向的,因此能访问第1个结点,从而也就能访问第2个结点,第3个结点……从这一点上看,链表的访问效率比较低。

链表与数组特点对比:
数组的优点:
数组中的元素在内存中是连续存放的,能根据数组的首地址计算出各数组元素的内存地址,所以可以直接用下标访问到数组元素;而链表中的元素在内存中通常是不连续存放的,因此不能被立即访问到。

链表的优点:
1、可伸缩性:数组一旦在内存分配空间之后,大小就不能改变;而链表是动态的,在需要的时候可以增加或删减结点;数组的空间可能很快就用完,而链表只有在系统没有足够的内存满足动态分配存储空间的请求时 才会达到全满的状态;
2、插入和删除操作:数组的插入和删除涉及到移动元素的操作,因此比较费时;而链表的插入和删除比较简单;

对链表的基本操作:
创建链表、检索(查找)结点、插入、删除结点和修改结点等。
(1)创建链表是指:从无到有地建立起一个链表,即往空链表中依次插入若干结点,并保持结点之间的前驱和后继关系。
(2)检索操作是指:按给定的结点索引号或检索条件,查找某个结点。
(3)插入操作是指:在结点ki-1与ki之间插入一个新的结点k 。
(4)删除操作是指:删除结点ki,使线性表的长度减1。
下面我们依次看一下这些操作。

  1. 创建:
    为了统一描述,先建立好结构及其代称方便之后描述:
typedef struct listnode list;
typedef list * listPtr;
struct listnode{
	int data;
	list *next;
};

【注】:创建的两种方法:
1.返回头指针值;listPtr create(){…}
2.执行对头指针地址值的改写;void create(listPtr *headPtr){…}

listPtr create()                        				//创建并返回头指针
{
    int num;
    list *headPtr=NULL,*currentPtr=NULL,*lastPtr=NULL;
    scanf("%d",&num);									//以输入数据为-1结束
    while(num!=-1){
        currentPtr=(listPtr)malloc(sizeof(list));		//给current分配新的空间,使其进一步变为一个存储信息的节点
        if(currentPtr!=NULL)
            currentPtr->data=num;
        if(headPtr==NULL)								//如果头指针为空,那么把current给他,并用lastPtr记录current的当前位置
        {
            headPtr=currentPtr;
            lastPtr=currentPtr;
        }
        else{											//否则,把上一个lastPtr的next指针和当前的节点连起来,然后继续用lastPtr记录current位置
            lastPtr->next=currentPtr;
            lastPtr=currentPtr;
        }
        scanf("%d",&num);
    }
    lastPtr->next=NULL;									//一定记得把链表尾节点的next指向空NULL
    return headPtr;
}

如果实在无法理解创建过程,可以这样想:比如lastPtr这个辅助指针,
lastPtr->nextPtr=currentPtr;
lastPtr=currentPtr;
这两句话其实是说:我之前构造的空间,它的表尾与下一个新空间进行了联通,那么现在就实现了链接,我需要让lastPtr继续服务,就必须让它继续充当“最后一个空间的牌面”,因而把新空间的地址交付给他,这一切完成的只是“利用名字找对地址,进而使地址名下的空间实现连通”的过程而已,一个节点的地址可以用多个名字表示的。

  1. 打印:
    顺序遍历,不为空就打印出来
void print(listPtr headPtr)                    //打印
{
    listPtr current=NULL;
    if(headPtr==NULL)
        printf("None\n");
    else{
        current=(listPtr)malloc(sizeof(list));
        current=headPtr;
        while(current!=NULL){
            printf("%d\n",current->data);
            current=current->next;
        }
    }
}
  1. 释放:
    创建时malloc申请地空间的常规操作
    使用辅助节点current记录地址位置以防头结点信息丢失
void destroy(listPtr headPtr){
	list *current=NULL;
	while(headPtr!=NULL){
		current=headPtr;
		headPtr=headPtr->next;
		free(current);
	}
}
  1. 插入:
    基本思路就是,从头开始往后遍历,设置current,用number进行遍历检索时,同时记录current前面一个节点prePtr,当number到达适合的位置时,就在prePtr和current节点之间插入新节点(当然,前提是这个链表本来就是升序的)
listPtr insert(listPtr headPtr)            //插入
{
    int num;
    list *prePtr=NULL,*newPtr=NULL,*current;
    scanf("%d",&num);
    newPtr=(listPtr)malloc(sizeof(list));
    newPtr->data=num;
    current=headPtr;
    while(current!=NULL&&num>current->data){
        prePtr=current;
        current=current->next;
    }
    if(prePtr==NULL){
        newPtr->next=current;
        headPtr=newPtr;
    }
    else if(current==NULL){
        current->next=newPtr;
        newPtr->next=NULL;
    }
    else{
        prePtr->next=newPtr;
        newPtr->next=current;
    }
    return headPtr;
}
  1. 删除:
    就是遍历,找适合条件的节点,然后把其前节点prePtr和其本身后节点next连起来
listPtr delete(listPtr headPtr)                //删除
{
    int num;
    scanf("%d",&num);
    list *prePtr=NULL,*current;
    current=headPtr;
    while(current!=NULL&&num!=current->data){
        prePtr=current;
        current=current->next;
    }
    if(prePtr==NULL){
        headPtr=headPtr->next;
    }
    else if(current==NULL){
        printf("You ass fool,there's no such kind of node.\n");
    }
    else{
        prePtr->next=current->next;
    }
    return headPtr;
}

链表的应用:

  1. 对链表内排序:升序链表的创建
    其实道理很简单,在创建过程中,没开辟一个新空间节点current,就把current插入到原链表中去,就实现了升序创建。
listPtr paixu(){
    int num;
    scanf("%d",&num);
    list *headPtr=NULL;
    while(num!=-1){
        headPtr=insert(headPtr,num);
        scanf("%d",&num);
    }
    return headPtr;
}


listPtr insert(listPtr headPtr,int num){
    int b=0;
    list *prePtr=NULL,*current,*newPtr=NULL;
    newPtr=(listPtr)malloc(sizeof(list));
    newPtr->data=num;
    if(headPtr==NULL){
        headPtr=newPtr;
        headPtr->next=NULL;            //否则程序在第二次进行插入时会卡死
    }
    else{
        current=headPtr;
        while(current!=NULL&&num>current->data){/*current!=NULL这句话要先写,
        C语言只要前者不行就不判断后者,这样,当current是NULL时不会造成
        “无法访问任何东西”,这被称为“逻辑短路”,不要越界访问!*/
            prePtr=current;
            current=current->next;
        }        
        if(prePtr==NULL){
            newPtr->next=current;
            headPtr=newPtr;
        }
        else if(current==NULL){
            prePtr->next=newPtr;
            newPtr->next=NULL;
        }
        else{
            prePtr->next=newPtr;
            newPtr->next=current;
        }
    }
    return headPtr;
}

我们也可以,冒泡排序,基本思路就是,记录要被交换的节点A的prePtr和节点B的prePtr和节点B自己的位置,三节点交换。这里先用辅助量count(链表元素个数)方便操作

listPtr order(listPtr headPtr,int count){
    int i,j;
    list *prePtr1=NULL,*prePtr2=NULL,*current=NULL;
    if(count==1)
        return headPtr;											//直接判断特殊情况,节省时间先
    else{
        for(i=count;i>0;i--){
            prePtr1=headPtr;
            prePtr2=prePtr1->next;
            current=prePtr2->next;
            for(j=0;j<i&&prePtr2->next!=NULL;j++){
                if(j==0&&prePtr1->data > prePtr2->data){		//如果判断的是第一、第二个节点且要交换
                    prePtr1->next=prePtr2->next;				//三节点交换法,不懂可以画箭头图分析
                    prePtr2->next=prePtr1;
                    headPtr=prePtr2;
                    prePtr1=headPtr;
                    prePtr2=prePtr1->next;
                    current=prePtr2->next;						//保持三个节点相对顺序,这样可以一直判断下去
                }
                if(j>0&&prePtr2->data > current->data){
                    prePtr1->next=current;
                    prePtr2->next=current->next;
                    current->next=prePtr2;
                }
                if(j>0){
                    prePtr1=prePtr1->next;
                    prePtr2=prePtr1->next;
                    current=prePtr2->next;
                }
            }
        }
    }
    return headPtr;
}
  1. 链表节点段的交换问题:
    上文只涉及单节点交换,接下来我们完成从[s1],[t1]到[s2],[t2]的片段交换。
    坑:1.如果s1=t1等怎么处理?我的算法由于记录了整8个节点所以相对安全
    2.如果t1=s2-1?s1=0?{
    情况一:t1=s2-1如图过程:在这里插入图片描述 在这里插入图片描述
    情况二:t1!=s2-1如图:在这里插入图片描述在这里插入图片描述
listPtr exchange(listPtr headPtr,int s1,int t1,int s2,int t2)
{
    list *prePtr1=NULL,*prePtr2=NULL,*leftPtr;
    list *preTemp1=NULL,*lastTemp1,*preTemp2=NULL,*lastTemp2;
    prePtr1 =find(headPtr,s1-1);        preTemp1 =find(headPtr,s1);
    lastTemp1=find(headPtr,t1);
    prePtr2 =find(headPtr,s2-1);        preTemp2 =find(headPtr,s2);
    lastTemp2=find(headPtr,t2);
    if(s2!=t1+1){
        leftPtr=lastTemp1->next;
        lastTemp1->next=lastTemp2->next;
        if(s1>1)
            prePtr1->next=preTemp2;
        lastTemp2->next=leftPtr;
        prePtr2->next=preTemp1;
    }
    else{
        if(s1>1)
            prePtr1->next=preTemp2;
        lastTemp1->next=lastTemp2->next;
        lastTemp2->next=preTemp1;
    }
    if(s1!=1)
        return headPtr;
    else
        return preTemp2;
}
listPtr find(listPtr headPtr,int where)				//找到第where个节点并返回其地址位置
{
    int count=0;
    list *current=NULL,*prePtr=NULL;
    current=headPtr;
    if(where==0)prePtr=headPtr;
    while(count<where&&current!=NULL){
        prePtr=current;
        current=current->next;
        count++;
    }
    return prePtr;
}

以下为双向节点链表(虽然不完全是):值得注意的是,对某些节点定义好内部pre指针的方向,将有助于大大简化操作

typedef struct node{
    int value;
    struct node *next;
    struct node *pre;
}Node;								//以及:不再需要find函数!
Node* exchange(Node *head,int s1,int t1,int s2,int t2){
    Node *ps1,*pt1,*ps2,*pt2;
    Node *p = head->next;
    for(int i = 1;p;p = p->next,i++){
        if(i==s1) ps1 = p;
        if(i==t1) pt1 = p;
        if(i==s2) ps2 = p;
        if(i==t2) pt2 = p;
    }
    Node *temp1 = ps1,*temp2 = ps2;
    ps1->pre->next = temp2;
    temp2->pre->next = temp1;
    Node *temp3 = pt1->next,*temp4 = pt2->next;
    pt2->next = temp3;
    pt1->next = temp4;				 //    三变量法交换
    return head;
}
其余略......
  1. 逆序输出问题:
    方法一:在创建的时候,每次制造完新节点,都让他接到head前面,再把head调到首位,循环此操作。
listPtr create(){
    list *headPtr,*current;
    int num;
    scanf("%d",&num);
    while(num!=-1){
        current=(listPtr)malloc(sizeof(list));
        current->data=num;
        if(headPtr==NULL){
            headPtr=current;
            headPtr->next=NULL;
        }
        else{
            current->next=headPtr;
            headPtr=current;
        }
        scanf("%d",&num);
    }
    return headPtr;
}

方法二:拆解,重构。从头开始,依次拆解一个节点的next指向,并指向新节点:其previous one。 就像是不断把head挪到新位置在这里插入图片描述

listPtr reverse(listPtr headPtr){
    list *prePtr,*current,*frontPtr;
    if(headPtr==NULL)
        return NULL;
    else{
        prePtr=headPtr;
        current=prePtr->next;
        headPtr->next=NULL;        //注意!因为此时head的位置是作为最终链表的尾节点
        while(current!=NULL){
            frontPtr=current->next;
            current->next=prePtr;
            prePtr=current;
            current=frontPtr;
        }
    }
    return prePtr;                   //别传错了,head此时只是尾节点
}

方法三:递归,不到最后一项不打印。先函数,后打印

void print(listPtr headPtr){
    list *current;
    current=headPtr;
    if(current!=NULL){
        print(current->next);
        printf("%d",headPtr->data);
    }
}
  1. 链表归并问题:
    思路:明确我想把head2插到head1里面去,实现过程:首先,针对head2的首数,看一看它在head1里能找到哪个夹缝塞进去,即对head1遍历,head2->data为标尺确定prePtr1和current1 然后看看从head2开始能有几个节点能进入夹缝,即对head2遍历,current1->data为标尺卡出prePtr2和current2,确定后,插入后,把head2换到current2位置,继续搜索插入。
    有关leftPtr:这是一个针对head的辅助变量,因为我们发现两个head都有可能被改写,而head2的改写可能是“丢失性的”,leftPtr能帮助我们控制好head2的地址得以保留。
listPtr merge(listPtr headPtr1,listPtr headPtr2){
    list *prePtr1,*prePtr2,*current1,*current2,*leftPtr;
    while(headPtr2!=NULL){
        leftPtr=headPtr2;
        prePtr1=NULL;
        prePtr2=NULL;
        current1=NULL;
        current2=NULL;
        find(headPtr1,leftPtr->data,&prePtr1,&current1);
        if(current1==NULL){
            prePtr1->next=leftPtr;
            headPtr2=NULL;
        }
        else{
            find(headPtr2,current1->data,&prePtr2,&current2);
            if(current1==headPtr1){
                prePtr2->next=headPtr1;
                headPtr1=leftPtr;
            }
            else{
                prePtr1->next=leftPtr;
                prePtr2->next=current1;
                headPtr2=current2;
            }
        }
    }
    return headPtr1;
}

void find(listPtr headPtr,int num,listPtr *prePtr,listPtr *current){	//相互查询找位子
    list *currentTemp,*preTemp;
    currentTemp=headPtr;
    preTemp=NULL;
    while(currentTemp!=NULL&&currentTemp->data<=num){
        preTemp=currentTemp;
        currentTemp=currentTemp->next;
    }
    *prePtr=preTemp;
    *current=currentTemp;
}
  1. 约瑟夫环问题:
    在一个“循环链表”里(即lastPtr->next=headPtr),每次让prePtr处于“零位”,那么在每次循环开始,我让current指针开始向后移位,并结合count递增,就能模拟“从一开始依次报数”了
    首先,第一次循环current,使current到达尾节点。第二次循环,初,用prePtr记录尾节点“零位”,current后移到达1,报数开始,如果可除名,除,利用prePtr,实现新“零位”。
    比如猴子选大王
listPtr create(int n){								//创建有n个节点的链表
    list *headPtr=NULL,*current=NULL,*lastPtr=NULL;
    int i;
    for(i=1;i<=n;i++){
        current=(listPtr)malloc(sizeof(list));
        current->data=i;
        if(headPtr==NULL){
            headPtr=current;
            lastPtr=current;
            headPtr->next=NULL;
        }
        else{
            lastPtr->next=current;
            lastPtr=current;
        }
    }
    lastPtr->next=headPtr;
    return headPtr;
}

listPtr choose(listPtr headPtr,int n,int m){
    list *prePtr,*current,*lastPtr;
    int count=0;
    current=headPtr;
    while(current->next!=headPtr)
        current=current->next;                    //找到最后一个区
    while(current->next!=current){
        prePtr=current;
        current=current->next;
        count++;
        if(count%m==0){
            prePtr->next=current->next;
            printf("It's '%d!\n",current->data);
            free(current);
            current=prePtr;
        }
    }
    printf("Our king:%d!!!\n",current->data);
    free(current);									
}
//因为是循环链表,除非告知n,要不释放起来比较麻烦,而边创建新链接边释放就方便得多,也符合“出局”要求

总之,链表的思想精髓在于 交换位置 而非 交换内容(当然交换内容也行,但锻炼不了思维方式),在某些对数据结构要求高的方面,链表是极其基础而重要的工具。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值