链表——灵活的“数组”(链表的操作)

来源:互联网 发布:淘宝新开店如何推广 编辑:程序博客网 时间:2024/05/16 19:14
本周第一次讲座,学长给我们简单的概述了数据结构和算法,然后对链表的一些操作进行了讲解,下来之后,我把原来书上的一些

链表的基本操作与链表的逆置,排序等操作结合起来,整理出来

  1. 链表是由结点构成的,关键是定义结点
  2. C语言程序设计上两大特例:①链表节点的定义②递归函数的定义。这两个违反了先定义再使用。

一、链表的分类     

        3.静态链表:各结点在程序中定义,不是临时开辟的(不是用malloc函数或者calloc函数动态开辟的空间),始终占着内存不放。

struct student{int number;                     //数据区域 struct student *next;          // 指针区域 };                                //定义结构体类型 int main(){struct student a,b,c,*head,*p;             //定义结点、头指针、和进行遍历的指针 a.number=1;b.number=2;c.number=3;head=&a;a.next=&b;b.next=&c;c.next=NULL;p=head;do{printf("d",p->number);             //进行遍历 p=p->next;}while(p!NULL); } 

 二、链表的基本操作

         4.动态链表的创建

(1)创建链表:从无到有建立起一个链表,即往空链表中一次插入若干结点,并保持结点之间的前驱和后继关系。顾名思义,链表就像一条铁环串成的链子,每个铁环是结点,连接下一个铁环,也可以理解为老师带领手拉手小朋友排队,头指针是老师,小朋友与小朋友之间拉着手。

第一次看的时候没有看懂,应该在纸上把链表的图画一画就可以明白。

struct num{int number;struct num *next; }; int icount;                            //全局变量标记结点的顺序  struct *create() { struct *pHead=NULL,*pEnd,*pNew; icount=0; pNew=pEnd=(struct*)malloc(sizeof(struct num)); scanf("%d",&pNew->number); while(pNew->number!=0)             //while(pNew->number>0)作用相同{icount++;if(icount==1)                  //判断是否是第一次加入结点也可以用if(pHead==NULL)进行判断 {    pNew->next=pHead;           //使指针指向为空,也可以pNew->next=NULL pEnd=pNew;pHead=pNew;}else{pNew->next=NULL;pEnd->next=pNew;pEnd=pNew;}pNew=(struct*)malloc(sizeof(struct num));//再次分配结点的内存空间,为下一个结点插入做准备 scanf("%d",&pNew->number); }  free(pNew); return pHead; }

         5.单链表的遍历输出

(2)检索操作:按给定的结点引导或检索条件,查找某个结点。如果找到指定的结点则称为检索成功;否则检索失败。自己理解是从链表的开头按照顺序一个一个检查,然后找到自己想要找到的结点,如果找到则检索成功,如果没找到就是检索失败。

 void print(struct num *pHead)  { struct num *pTemp;      int n=1;                            //表示链表中结点的序号  pTemp=pHead; while(pTemp!=NULL) { printf("%d",pTemp->num); pTemp=pTemp->next;                 //遍历的关键,移动临时指针到下一个结点  n++;} }

        6.单链表的插入

(3)插入操作:在结点N(i)和N(i+1)之间插入一个新的结点N,使线性表的长度增加1,且N(i)和N(i+1)的逻辑关系发生变化:

插入前N(i)是N(i+1)的前驱,N(i+1)是N(i)的后继;插入后新的结点N成为N(i)的后继,N(i+1)的前驱。

插入原则:

①插入操作不应破坏原链接关系

②插入的结点应该在它该在的位置(按照顺序排好,如果不要求顺序,则可以任意插入)

实现方法:

应该有一个插入位置的查找子过程

共有3中插入情况:

①插入结点在链表最前端

 struct num *Insert(struct num *pHead) { struct num *pNew; pNew=(struct*)malloc(sizeof(struct num)); pNew->next=pHead;                 //新结点指针指向原来的首结点  pHead=pNew;                       //头指针指向新节点  icuont++;                         //增加链表节点数量  return pHead;  }

②插入节点在链表中间

 struct num *Insert(struct num *pHead,int n){struct num *p=pHead,*PNew;             //两个指针,*P是插入结点的前一个结点 while(p&&p->number!=n)                 //查找要插入的位置 p=p->next;                             //下移到下一个结点处 pNew=(struct*)malloc(sizeof(struct num));scanf("%d",&pNew->number);pNew->next=p->next ;                      //新结点指向下一个结点 p->next=pNew;                            // 上一个结点指向新结点 icount++;                                //链表长度增加 return pHead;}

③插入节点在链表尾端

struct num *Insert(struct num *pHead){struct num *p=pHead,*pNew;while(p&&p->next!=NULL)p=p->next;pNew=(struct*)malloc(sizeof(struct num));scanf("%d",&pNew->number);p->next=pNew;                         // 尾结点指向新结点 pNew->next=NULL;                      //新结点指向空指针,变为尾结点 icount++;return pHead;}


         7.单链表的删除

(4)删除操作:删除结点N(i)后,使线性表的长度减1,且N(i-1)N(i)N(i+1)的逻辑关系发生变化:

删除前,N(i)是N(i+1)的前驱,N(i-1)的后继;删除后,N(i-1)成为N(i+1)的前驱,N(i+1)成为N(i-1)的后继。

删除的原则:

不改变原来排列的顺序,只是从链表中分离开来,撤销原来的链接关系。

两种情况:

①要删的结点是头指针所指的结点则直接操作;

②不是头结点,要依次往下找。另外考虑:空表和找不到要删除的结点

需要两个临时指针:
P1:判断指向的结点是不是要删除的结点(用于寻找);

P2:始终指向P1前面的一个结点;

 void Delete(struct num *pHead,int n) { int i; struct num *pTemp;               //临时指针,用于查找所要删除的结点  struct num *pPre;               //表示要删除的结点前的结点   pTemp=pHead;                    //得到链表的头结点  for(i=1;i<n;i++) { pPre=pTemp;                 //pPre跟进pTemp  pTemp=pTemp->next;          //pTemp前进下移到下一个结点  } pPre->next=pTemp->next;          // 将要删除的结点两边的结点链接  free(pTemp); icount--;                             //减少链表中结点的个数  }

 三、链表的排序          

   8.单链表的反序

        单向链表的反序图示:
                ---->[1]---->[2]---->[3]...---->[n]---->[NULL](原链表)
                head   1->next  2->next  3->next   n->next

                [NULL]<----[1]<----[2]<----[3]<----...[n]<----(反序后的链表)
                1->next  2->next  3->next   n->next  head

反序也就是头指针指向最后一个结点,然后最后一个结点指向倒数第二个结点,以此类推,直到第一个结点指向空指针

           实现方法:

(1)首先考虑如何能实现整个反序过程,很容易想到必须有一个指针P1进行读取每个结点,将每个结点的指向改变,还要有一个指针P2来存储每次要更改的那个指向,然后还要有一个临时指针P用来存储需要更改的指针的下一个指针。

(2)先将各个功能指针初始化,P1指向头指针,P2指向空指针;

(3)先用P把下一个要截取的结点储存起来,然后开始改变当前结点的指向

(4)改变要指向的结点P2,P1移动到下一个结点,循环遍历

struct num *Reverse(struct *pHead){struct *p1;              //用来遍历结点一个一个截取结点 struct *p2;             //用来储存要指向的结点 struct *p;             //用来保存下一个要截取的结点 p1=pHead;             //从头指针开始遍历 p2=NULL;             //第一个要指向空指针 while(p1!=NULL){p=p1->next;          //保存下一个结点 p1->next=p2;        //改变该结点的指向 p2=p1;             //将下一个要指向的结点替换掉上一个 p1=p;             //移动下标到下一个结点 }pHead=p1;                //头指针指向最后,改变顺序呢 return pHead;}

9.单链表的选择排序

(1)基本思想:

对单链表进行选择排序的基本思想和数组基本一样,选择一个结点或者元素和其他进行比较,每次找出剩下成员中最小的那个,因为单链表和数组有差异,因此在一些操作上有差别,对于链表,则是选出一个结点进行比较,然后将最小的结点放入另一个空链表中,依次类推,直到将原链表排成有序链表。

(2)

单向链表的选择排序图示:
         ---->[1]---->[3]---->[2]...---->[n]---->[NULL](原链表)
         head   1->next  3->next  2->next   n->next

         ---->[NULL](空链表)
        first
        tail

         ---->[1]---->[2]---->[3]...---->[n]---->[NULL](排序后链表)
         first   1->next  2->next  3->next   tail->next

(3)实现方法:

1.从第一个结点开始,找到最小的,放进另一个空链表中

2.空链表中放入第一个结点,形成一个有序链表,并将找到的结点从原链表中分离出来,不在让其参与下一次寻找

3.继续在原链表中找最小的,将其放入 有序链表的尾指针的next,然后将其标记为尾指针

4.需注意:

①所找到最小的结点是否是头结点,需要分情况讨论

②所放入的结点是否是第一个放入的结点,也需要分情况讨论

struct num *SelectSort(struct num *pHead){struct num *first;                   // 排序后有序链表的头指针 struct num *tail;                   //排序后有序链表的尾指针 struct num *p_min;                 //用来储存所找到 最小结点的前驱结点 struct num *min;                  //储存最小结点 struct num *p;                    //提取每一个结点记性比较 first=NULL;                       //将未放入结点的头指针指向空 while(pHead!=NULL)                //当所有结点都被分离以后头指针指向空,跳出循环 {for(p=pHead,min=pHead;p->next!=NULL;p=p->next)//循环遍历链表中的结点,判断p->next是否 指到最后的节点 {if(p->next->number<min->number){p_min=p;min=p->next;              //找出最小的结点,并保存其前驱结点 }}if(first==NULL)                  //如果有序链表还是一个空链表 {first=min;                  //第一个结点即是头指针又是尾指针         tail=min;}else{tail->next=min;            //将取出的结点和有序链表链接起来并标记为尾指针 tail=min;}if(min==pHead)                 //如果找到最小的结点是头结点 pHead=pHead->next;        //将头结点指向第二个结点 elsep_min->next=min->next;    //否则就是去除的结点前驱指向其后继结点 }if(first!=NULL)                         tail->next=NULL;            //单链表最后一个结点指向空指针 pHead=first;return pHead;}

10.单链表的插入排序

(1)基本思想:

依然和数组类似,显示取出第一个结点作为有序数组,然后依次将后面的结点插入到相应位置

(2)

单向链表的直接插入排序图示:
         ---->[1]---->[3]---->[2]...---->[n]---->[NULL](原链表)
        head   1->next  3->next  2->next   n->next

         ---->[1]---->[NULL](从原链表中取第1个节点作为只有一个节点的有序链表)
        head
        图11

        ---->[3]---->[2]...---->[n]---->[NULL](原链表剩下用于直接插入排序的节点)
        first   3->next  2->next   n->next
        图12

        ---->[1]---->[2]---->[3]...---->[n]---->[NULL](排序后链表)
        head   1->next  2->next  3->next   n->next

struct num *InsertSort(struct num *pHead){struct num *first;              //作为剩下待排序的链表的头指针 struct num *t;                  //临时变量,保存准备插入的结点 struct num *p,*q;               //临时变量,表示要插入位置前后两个结点 first=pHead->next;              //将第一个结点看做有序数列,从第二个结点开始待插入 pHead->next=NULL;               //未插入结点时,头指针指向空 while(first!=NULL)              //遍历剩下未插入的结点 {for(t=first,q=pHead;((q!=NULL)&&(q->number<t->number));p=q;q=q->next);//此循环是在有序链表中找到插入的位置 first=first->next;             //将待插入的结点从无序链表中分离 if(q==pHead)                   //插在第一个结点之前 {pHead=t;}else{p->next=t;              //p是q的前驱 }t->next=q;                  //完成插入操作  }  return pHead;}

11.单链表冒泡排序

(1)基本思想:

对链表的冒泡排序和对数组冒泡排序的基本思想是一样的,每次比较相邻两个数的大小,进行交换。

(2)单链表冒泡排序示意图

单向链表的冒泡排序图示:
        ---->[1]---->[3]---->[2]...---->[n]---->[NULL](原链表)
       head   1->next  3->next  2->next   n->next

       ---->[1]---->[2]---->[3]...---->[n]---->[NULL](排序后链表)
       head   1->next  2->next  3->next   n->next

       有N个节点的链表冒泡排序

      任意两个相邻节点p、q位置互换图示:
      假设p1->next指向p,那么显然p1->next->next就指向q,
      p1->next->next->next就指向q的后继节点,我们用p2保存
      p1->next->next指针。即:p2=p1->next->next,则有:
       [  ]---->[p]---------->[q]---->[  ](排序前)
       p1->next  p1->next->next  p2->next
       

       [  ]---->[q]---------->[p]---->[  ](排序后)

(3)实现方法:

(1)基本实现思路是,相邻两个结点,比较得出较小的结点,将其从原来链表中分离,然后插入到较大结点之前,要注意链表的连续和完整性

(2)设置一个标记可以将每趟排完序的最后位置记录下来,下一趟可以到那个位置停止,那个位置以后的结点都已经有序。

struct num *BubbleSort(struct num *pHead){struct num *flag;         //控制循环比较终止位置的标志 struct num *p;           //临时指针变量,用来储存已经排好序的位置 struct num *p1,p2;p1=(struct num *)malloc(sizeof(struct num));p1->next=pHead;         //这里增加一个结点是给要排序的链表增加一个前驱,方便对前两个结点进行排序pHead=p1;               //排序完成后将P1结点释放掉 for(flag=NULL;flag!=pHead;flag=p)           //设置标志,控制循环趟数 {for(p1=pHead;p1->next->next!=flag;p1=p1->next)    //循环遍历到标记处 {if(p1->next->number>p1->next->next->number)  //比较相邻两个结点的大小 {p2=p1->next->next;                       //取出后一个结点 p1->next->next=p2->next;                 //将较大结点和较小结点的后驱连接 p2->next=p1->next;                       //取出的结点放到较大结点之前 p1->next=p2;                              //较小结点和较大结点的前驱连接 p=p1->next->next;                        //记录下每次排序的位置,传给flag }}}p1=pHead;               //把P1原来储存的信息去掉 pHead=pHead->next;      //将头指针指向原来的头结点 free(p1);             //释放P1 p1=NULL;           //p1置为空,保证不产生“野指针”,即地址不确定的指针 return pHead;}


       12.有序链表插入结点

有序链表插入节点示意图:

        ---->[NULL](空有序链表)
        head

       图18:空有序链表(空有序链表好解决,直接让head指向它就是了。)

       以下讨论不为空的有序链表。
        ---->[1]---->[2]---->[3]...---->[n]---->[NULL](有序链表)
        head   1->next  2->next  3->next   n->next

       图18:有N个节点的有序链表

       插入node节点的位置有两种情况:一是第一个节点前,二是其它节点前或后。

       ---->[node]---->[1]---->[2]---->[3]...---->[n]---->[NULL]
       head  node->next  1->next  2->next  3->next   n->next

       图19:node节点插在第一个节点前

       ---->[1]---->[2]---->[3]...---->[node]...---->[n]---->[NULL]
      head   1->next  2->next  3->next    node->next  n->next

struct num *SortInsert (struct num *pHead, struct num *node){struct num *p;//P用来保存当前要比较的结点 struct num *t;//临时指针变量if (pHead==NULL)//处理空的有序链表{pHead=node;node->next=NULL;n+= 1;//插入完毕,节点总数加1return pHead;}p=pHead;  //当有序链表不为空while(p->num <node->num&&p!=NULL)   //p指向的节点的学号比插入节点的学号小,并且它不等于NULL{t=p;  //保存当前节点的前驱,以便后面判断后处理p=p->next;//后移一个节点}if (p==pHead)//刚好插入第一个节点之前{node->next=p;pHead=node;}else //插入其它节点之后{t->next=node;//把node节点加进去node->next=p;}n+= 1;//插入完毕,节点总数加return pHead;}

四、循环链表

单链表最后一个结点的指针指向NULL,循环链表的最后一个结点的指针指向链表头结点,首尾相连,形成数据链。

与单链表的不同点:

(1)链表的建立:单链表需要创建一个头结点,专门存放第一个结点的地址,单链表的尾指针的指针域指向NULL,;而循环链表的建立,不需要专门的头结点,让最后一个结点的指针域指向链表的头结点即可。

(2)链表表尾的判断:单链表判断结点是否为表尾结点,只需判断结点的指针域是否为NULL,如果是,则为尾结点,否则不是。而循环链表判断是否尾结点,则是判断该节点的指针域是否指向链表头结点。

五、双向链表

双向两链表也是基于单链表,单链表有一个头结点,一个尾结点,双链表有两个指针域,一个指向左边一个指向右边,

一个存储直接后继结点地址,一般称为右链域,一个存储直接前驱结点地址,一般成为左链域。

13.双向循环链表创建

struct num{int number;struct num *rlink;struct num *llink;};struct num *creat(int n){struct num *p,*h,*s;    //h代表头结点,S代表新结点,p则储存新结点的前驱 int i;if(h=(struct*)malloc(sizeof(struct num))==NULL)     //判断开辟空间是否成功  exit(0); h->llink=NULL;         //头结点初始化  h->rlink=NULL;          //头结点初始化  p=h; for(i=0;i<n;i++) { if(s=(struct*)malloc(sizeof(struct num))==NULL) //开辟新结点  exit(0); p->rlink=s;          //新结点前驱向右指向新结点  printf("请输入第%d个人的姓名",i+1); scanf("%d",s->number);      //输入新结点里的信息  s->llink=p;                  //新结点向左指向前驱  s->rlink=NULL;               //新结点向右指向空 p=s;                         //将创建好的新结点作为下一个新结点的前驱  } h->llink=s;                     //头结点向左指向最后一个结点  p->rlink=h;                     //最后一个结点向右指向头结点,连接完成  return(h);}

14.双向链表查找

从表头结点往后依次比较各结点数据域的值,若是该特定值。则返回结点的指针,否则继续往后查,直到表尾

struct num *search(struct num *pHead,int n){struct num *p;           //行走指针,一个一个结点比较 int n;                   //要得到的那个数 p=pHead->rlink;          //从第一个结点开始 while(p!=h)              //当P走完一圈 {y=p->number;          if(n==y)             //进行比较 return(p);elsep=p->rlink;         //继续遍历 }}

15.双向列表插入

双向链表插入结点和单链表插入结点方法基本相同,只是指针域要分左右,比如要在p,q之间插入结点s,只需把p右链域指向s,s的左链域指向p,s的右链域指向q,q的左链域指向s即可

void Intset(struct num *p){int n;struct num *s;if(s=(struct*)malloc(sizeof(struct num))==NULL) //开辟空间 exit(0);scanf("%d",&n); s->number=n;s->rlink=p->rlink;                //将新结点的向右指向后一个结点 p->rlink=s;                       //前一个结点指向新结点 s->llink=p;                       //新结点指向前一个结点 (s->rlink)->llink=s;              //后一个结点向左指向新结点 } 

16.双链表的删除

双链表的删除和单链表的删除也类似,将要删除的链表跳过即可,需要注意的是在删除的时候分清左右链域

比如:s、p、q三个连续结点,要删除p,只需将s的右链域指向q,q的左链域指向s,并释放p结点就可以了

void del(struct num *p){(p->rlink)->llink=p->llink;//要删除结点P的后继向左指向P的前驱 (p->llink)->rlink=p->rlink;//要删除结点P的前驱向右指向P的后继 free(p);                   //将P开辟的空间释放 } 

以上是链表的一些操作,如果以上内容有错误,欢迎大家指出,谢谢大家吐舌头


0 0
原创粉丝点击