数据结构--链表

来源:互联网 发布:在ubuntu安装eclipse 编辑:程序博客网 时间:2024/06/06 03:19

链表中的数据是以节点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。
链表的结点结构
┌──┬──┐
│data│next│
└──┴──┘
 data域–存放结点值的数据域
 next域–存放结点的直接后继的地址(位置)的指针域(链域)

以“结点的序列”表示线性表称作线性链表(单链表)

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表示它是指向某一结点的指针

①生成结点变量的标准函数
 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后继结点

单链表的运算

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) 尾插法建表
① 算法思路
 从一个空表开始,重复读入数据,生成新结点,将读入数据存放在新结点的数据域中,然后将新结点插入到当前链表的表尾上,直到读入结束标志为止。
② 具体算法实现

 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* 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的结点       }

(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的结点       }

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;       }  

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)时,才能确定被删结点存在。
 
循环链表是另一种形式的链式存贮结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。(循环链表)

仅设尾指针的单循环链表
 用尾指针rear表示的单循环链表对开始结点a1和终端结点an查找时间都是O(1)。而表的操作常常是在表的首尾位置上进行,因此,实用中多采用尾指针表示单循环链表。带尾指针的单循环链表可见下图。

若在单链表或头指针表示的单循环表上做这种链接操作,都需要遍历第一个链表,找到结点an,然后将结点b1链到an的后面,其执行时间是O(n)。若在尾指针表示的单循环链表上实现,则只需修改指针,无须遍历,其执行时间是O(1)。
相应的算法如下:

LinkList Connect(LinkList A,LinkList B)        {//假设AB为非空循环链表的尾指针          LinkList p=A->next;//①保存A表的头结点位置          A->next=B->next->next;//B表的开始结点链接到A表尾          free(B->next);//③释放B表的头结点          B->next=p;//return B;//返回新循环链表的尾指针    } 

双(向)链表中有两条方向不同的链,即每个结点中除next域存放后继结点地址外,还增加一个指向其直接前趋的指针域prior。
注意:
 ①双链表由头指针head惟一确定的。
 ②带头结点的双链表的某些运算变得方便。
 ③将头结点和尾结点链接起来,为双(向)循环链表。
形式描述

    typedef struct dlistnode{          DataType data;          struct dlistnode *prior,*next;       }DListNode;     typedef DListNode *DLinkList;     DLinkList head;

双向链表的前插和删除本结点操作
①双链表的前插操作

void DInsertBefore(DListNode *p,DataType x)       {//在带头结点的双链表中,将值为x的新结点插入*p之前,设p≠NULL         DListNode *s=malloc(sizeof(DListNode));//①         s->data=x;//②         s->prior=p->prior;//③         s->next=p;//④         p->prior->next=s;//⑤         p->prior=s;//⑥        }

双链表上删除结点*p自身的操作

void DDeleteNode(DListNode *p)       {//在带头结点的双链表中,删除结点*p,设*p为非终端结点          p->prior->next=p->next;//①           p->next->prior=p->prior;//②           free(p);//③       } 

注意:
 与单链表上的插入和删除操作不同的是,在双链表中插入和删除必须同时修改两个方向上的指针。
 上述两个算法的时间复杂度均为O(1)。
这里写图片描述

0 0
原创粉丝点击