单链表

单链表

1、链接存储方法
     链接方式存储的线性表简称为链表(Linked List)。
     链表的具体存储表示为:
  ① 用一组任意的存储单元来存放线性表的结点(这组存储单元既可以是连续的,也可以是不连续的)
  ② 链表中结点的逻辑次序和物理次序不一定相同。为了能正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址(或位置)信息(称为指针(pointer)或链(link))
注意:
  
链式存储是最常用的存储方式之一,它不仅可用来表示线性表,而且可用来表示各种非线性的数据结构。

2、链表的结点结构
  ┌──┬──┐
  │data│next│
  └──┴──┘ 
       data域--存放结点值的数据域
       next域--存放结点的直接后继的地址(位置)的指针域(链域)
注意:
     ①链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。
     ②每个结点只有一个链域的链表称为单链表(Single Linked List)。
【例】线性表(bat,cat,eat,fat,hat,jat,lat,mat)的单链表示如示意图

     

3、头指针head和终端结点指针域的表示
     单链表中每个结点的存储地址是存放在其前趋结点next域中,而开始结点无前趋,故应设头指针head指向开始结点。
注意:
     
链表由头指针唯一确定,单链表可以用头指针的名字来命名。
【例】头指针名是head的链表可称为表head。
  终端结点无后继,故终端结点的指针域为空,即NULL。

4、单链表的一般图示法
     由于我们常常只注重结点间的逻辑顺序,不关心每个结点的实际位置,可以用箭头来表示链域中的指针,线性表(bat,cat,fat,hat,jat,lat,mat)的单链表就可以表示为下图形式。

       
5、单链表类型描述
  typedef char DataType; //假设结点的数据域类型为字符
  typedef struct node{   //结点类型定义
       DataType data;    //结点的数据域
       struct node *next;//结点的指针域
     }ListNode;
  typedef ListNode *LinkList;
  ListNode *p;
  LinkList head;
  注意:
     ①LinkList和ListNode *是不同名字的同一个指针类型(命名的不同是为了概念上更明确)
     ②LinkList类型的指针变量head表示它是单链表的头指针
     ③ListNode *类型的指针变量p表示它是指向某一结点的指针

6、指针变量和结点变量

┌────┬────────────┬─────────────┐ 
│    │    指针变量    │     结点变量      │
├────┼────────────┼─────────────┤
│  定义  │在变量说明部分显式定义  │在程序执行时,通过标准   
│        │                        │函数malloc生成           
├────┼────────────┼─────────────┤
│  取值  │ 非空时,存放某类型结点 │实际存放结点各域内容     
│        │的地址                  │                         
├────┼────────────┼─────────────┤
│操作方式│ 通过指针变量名访问     │ 通过指针生成、访问和释放 │
└────┴────────────┴─────────────┘
   
①生成结点变量的标准函数
     p=( ListNode *)malloc(sizeof(ListNode));
//函数malloc分配一个类型为ListNode的结点变量的空间,并将其首地址放入指针变量p中
②释放结点变量空间的标准函数 
     free(p);//释放p所指的结点变量空间
③结点分量的访问 
     
利用结点变量的名字*p访问结点分量
 方法一:(*p).data和(*p).next
 方法二:p-﹥data和p-﹥next
④指针变量p和结点变量*p的关系 
 
    指针变量p的值——结点地址
     结点变量*p的值——结点内容
     (*p).data的值——p指针所指结点的data域的值
     (*p).next的值——*p后继结点的地址
  *((*p).next)——*p后继结点
注意:
     ① 若指针变量p的值为空(NULL),则它不指向任何结点。此时,若通过*p来访问结点就意味着访问一个不存在的变量,从而引起程序的错误。
     ② 有关指针类型的意义和说明方式的详细解释,【参考C语言的有关资料】。

 

  下一页

单链表的运算

1、建立单链表
     
假设线性表中结点的数据类型是字符,我们逐个输入这些字符型的结点,并以换行符'\n'为输入条件结束标志符。动态地建立单链表的常用方法有如下两种:

(1) 头插法建表
① 算法思路
     从一个空表开始,重复读入数据,生成新结点,将读入数据存放在新结点的数据域中,然后将新结点插入到当前链表的表头上,直到读入结束标志为止。
     

     具体方法【参见动画演示
注意:
     该方法生成的链表的结点次序与输入顺序相反。

② 具体算法实现
    LinkList CreatListF(void)
      {//返回单链表的头指针
          char ch;
          LinkList head;//头指针
          ListNode *s;  //工作指针
          head=NULL;    //链表开始为空
          ch=getchar(); //读入第1个字符
          while(ch!='\n'){
              s=(ListNode *)malloc(sizeof(ListNode));//生成新结点
              s->data=ch;   //将读入的数据放入新结点的数据域中
              s->next=head;
              head=s;
              ch=getchar();  //读入下一字符
            }
          return head;
      

(2) 尾插法建表 
① 算法思路 
     
从一个空表开始,重复读入数据,生成新结点,将读入数据存放在新结点的数据域中,然后将新结点插入到当前链表的表尾上,直到读入结束标志为止。

      
     具体方法【参见动画演示
注意:
    ⒈采用尾插法建表,生成的链表中结点的次序和输入顺序一致
    ⒉必须增加一个尾指针r,使其始终指向当前链表的尾结点

② 具体算法实现 
    
LinkList CreatListR(void)
      {//返回单链表的头指针
          char ch;
          LinkList head;//头指针
          ListNode *s,*r;  //工作指针
          head=NULL;    //链表开始为空
          r=NULL;//尾指针初值为空
          ch=getchar(); //读入第1个字符
          while(ch!='\n'){
              s=(ListNode *)malloc(sizeof(ListNode));//生成新结点
              s->data=ch;   //将读入的数据放入新结点的数据域中
           if (head!=NULL)
               head=s;//新结点插入空表
           else
               r->next=s;//将新结点插到*r之后
              r=s;//尾指针指向新表尾
           ch=getchar();  //读入下一字符
         }//endwhile
        if (r!=NULL)
             r->next=NULL;//对于非空表,将尾结点指针域置空head=s;
         return head;
    
注意:
   
⒈开始结点插入的特殊处理
       由于开始结点的位置是存放在头指针(指针变量)中,而其余结点的位置是在其前趋结点的指针域中,插入开始结点时要将头指针指向开始结点。
   ⒉空表和非空表的不同处理
      若读入的第一个字符就是结束标志符,则链表head是空表,尾指针r亦为空,结点*r不存在;否则链表head非空,最后一个尾结点*r是终端结点,应将其指针域置空。

(3) 尾插法建带头结点的单链表 
①头结点及作用
     
头结点是在链表的开始结点之前附加一个结点。它具有两个优点:
    ⒈由于开始结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作就和在表的其它位置上操作一致,无须进行特殊处理;
    ⒉无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域空),因此空表和非空表的处理也就统一了。

②带头结点的单链表

     
    

注意:
  
头结点数据域的阴影表示该部分不存储信息。在有的应用中可用于存放表长等附加信息。

③尾插法建带头结点链表算法 
  
LinkList CreatListR1(void)
      {//用尾插法建立带头结点的单链表
          char ch;
          LinkList head=(ListNode *)malloc(sizeof(ListNode));//生成头结点
          ListNode *s,*r;  //工作指针
          r=head;    // 尾指针初值也指向头结点
          while((ch=getchar())!='\n'){
              s=(ListNode *)malloc(sizeof(ListNode));//生成新结点
              s->data=ch;   //将读入的数据放入新结点的数据域中
              r->next=s;
              r=s;
            }
          r->next=NULL;//终端结点的指针域置空,或空表的头结点指针域置空
          return head;
      
注意:
     
上述算法里,动态申请新结点空间时未加错误处理,这对申请空间极少的程序而言不会出问题。但在实用程序里,尤其是对空间需求较大的程序,凡是涉及动态申请空间,一定要加入错误处理以防系统无空间可供分配。

(4) 算法时间复杂度 
     以上三个算法的时间复杂度均为0(n)。

2.单链表的查找运算 
(1)按序号查找
① 链表不是随机存取结构
     在链表中,即使知道被访问结点的序号i,也不能像顺序表中那样直接按序号i访问结点,而只能从链表的头指针出发,顺链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构。

② 查找的思想方法
     计数器j置为0后,扫描指针p指针从链表的头结点开始顺着链扫描。当p扫描下一个结点时,计数器j相应地加1。当j=i时,指针p所指的结点就是要找的第i个结点。而当p指针指为null且j≠i时,则表示找不到第i个结点。
注意:
     头结点可看做是第0个结点。

③具体算法实现
 
    ListNode* GetNode(LinkList head,int i)
     {//在带头结点的单链表head中查找第i个结点,若找到(0≤i≤n),
      //则返回该结点的存储位置,否则返回NULL。
      int j;
      ListNode *p;
      p=head;j=0;//从头结点开始扫描
      while(p->next&&j<i){//顺指针向后扫描,直到p->next为NULL或i=j为止
          p=p->next;
          j++;
        }
      if(i==j)
         return p;//找到了第i个结点
      else return NULL;//当i<0或i>0时,找不到第i个结点
    

④算法分析

     算法中,while语句的终止条件是搜索到表尾或者满足j≥i,其频度最多为i,它和被寻找的位置有关。在等概率假设下,平均时间复杂度为:

         单链表 


(2) 按值查找
①思想方法 
    
 从开始结点出发,顺着链逐个将结点的值和给定值key作比较,若有结点的值与key相等,则返回首次找到的其值为key的结点的存储位置;否则返回NULL。

②具体算法实现

    ListNode* LocateNode (LinkList head,DataType key)
      {//在带头结点的单链表head中查找其值为key的结点
        ListNode *p=head->next;//从开始结点比较。表非空,p初始值指向开始结点
        while(p&&p->data!=key)//直到p为NULL或p->data为key为止
             p=p->next;//扫描下一结点
         return p;//若p=NULL,则查找失败,否则p指向值为key的结点
       }

③算法分析

     该算法的执行时间亦与输入实例中key的取值相关,其平均时间复杂度分析类似于按序号查找,为O(n)。

3.插入运算
(1)思想方法
     插入运算是将值为x的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间。
具体步骤: 
     
(1)找到ai-1存储位置p
     (2)生成一个数据域为x的新结点*s
     (3)令结点*p的指针域指向新结点
     (4)新结点的指针域指向结点ai
     
  
具体插入过程【参见动画演示

(2)具体算法实现

    void InsertList(LinkList head,DataType x,int i)
      {//将值为x的新结点插入到带头结点的单链表head的第i个结点的位置上
        ListNode *p;
        p=GetNode(head,i-1);//寻找第i-1个结点
        if (p==NULL)//i<1或i>n+1时插入位置i有错
           Error("position error");
        s=(ListNode *)malloc(sizeof(ListNode));
        s->data=x;s->next=p->next;p->next=s;
        

(3)算法分析

     算法的时间主要耗费在查找操作GetNode上,故时间复杂度亦为O(n)。

4.删除运算
(1)思想方法
     删除运算是将表的第i个结点删去。
具体步骤: 
   
 (1)找到ai-1的存储位置p(因为在单链表中结点ai的存储地址是在其直接前趋结点ai-1的指针域next中)
    (2)令p->next指向ai的直接后继结点(即把ai从链上摘下)
    (3)释放结点ai的空间,将其归还给"存储池"。
    
        

具体操作过程【参见动画演示

(2)具体算法实现

    void DeleteList(LinkList head,int i)
      {//删除带头结点的单链表head上的第i个结点
         ListNode *p,*r;
         p=GetNode(head,i-1);//找到第i-1个结点
         if (p==NULL||p->next==NULL)//i<1或i>n时,删除位置错
              Error("position error");//退出程序运行
         r=p->next;//使r指向被删除的结点ai
         p->next=r->next;//将ai从链上摘下
         free(r);//释放结点ai的空间给存储池
      
注意:
     设单链表的长度为n,则删去第i个结点仅当1≤i≤n时是合法的。
     当i=n+1时,虽然被删结点不存在,但其前趋结点却存在,它是终端结点。因此被删结点的直接前趋*p存在并不意味着被删结点就一定存在,仅当*p存在(即p!=NULL)且*p不是终端结点(即p->next!=NULL)时,才能确定被删结点存在。

(3)算法分析

     算法的时间复杂度也是O(n)。
     链表上实现的插入和删除运算,无须移动结点,仅需修改指针。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

huangleijay

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值