数据结构入门----链表的概念及应用

链表的表示

链式存储特点

逻辑上相邻,物理上不一定相邻

链表存放示意图:

每个存储结点都包含两部分:数据域和指针域(链域) 。

在单链表中,除了首元结点外,任一结点的存储位置由         其直接前驱结点的链域的值        指示。

与链式存储有关的术语

1)结点:数据元素的存储映像。由数据域和指针域两部分组成;

2)链表: n 个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构。

3)单链表、双链表、多链表、循环链表:

结点只有一个指针域的链表,称为单链表线性链表

有两个指针域的链表,称为双链表

有多个指针域的链表,称为多链表

首尾相接的链表称为循环链表

4)头指针、头结点和首元结点

头指针是指向链表中第一个结点(或为头结点或为首元结点)的指针。

单链表可由一个头指针唯一确定。

头结点是在链表的首元结点之前附设的一个结点;数据域内只放空表标志和表长等信息;

首元结点是指链表中存储线性表第一个数据元素a1的结点。

无头结点 有头结点

在链表中设置头结点有什么好处?

头结点即在链表的首元结点之前附设的一个结点,该结点的数据域中不存储线性表的数据元素,其作用是为了对链表进行操作时,可以对空表、非空表的情况以及对首元结点进行统一处理,编程更方便。  

(1) 对带头结点的链表,在表的任何结点之前插入结点或删除表中任何结点,所要做的都是修改前一结点的指针域,因为任何元素结点都有前驱结点。若链表没有头结点,则首元素结点没有前驱结点,在其前插入结点或删除该结点时操作会复杂些。  

(2) 对带头结点的链表,表头指针是指向头结点的非空指针,因此空表与非空表的处理是一样的。这样可以区分空链表和空指针,也减少了程序的复杂性和出现bug的机会。

在进行删除操作时,L为头指针,p指针指向被删结点,q指针指向被删结点的前驱,对于非空的单链表:

1.带头结点时 删除第1个结点(q指向的是头结点):q->next=p->next; free(p);

删除第i个结点(i不等于1):q->next=p->next;free(p);

2.不带头结点时  删除第1个结点时(q为空):L=p->next; free(p);

删除第i个结点(i不等于1):q->next=p->next;free(p);

结论:带头结点时,不论删除哪个位置上的结点,用到的代码都一样;不带头结点时,删除第1个元素和删除其它位置上的元素用到的代码不同,相对比较麻烦。

无头结点时,当头指针的值为空时表示空表;

有头结点时,当头结点的指针域为空时表示空表。

 头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值。

链表的数据元素有两个域,不再是简单数据类型,编程时该如何表示?因每个结点至少有两个分量,所以要采用结构数据类型。

单链表存储结构

Typedef struct Lnode {
     ElemType         data;         //数据域
     struct Lnode   *next;        //指针域
}Lnode, *LinkList;               // LinkList为指向Lnode类型的指针

补充:结构数据类型及其C语言表示法

每个结点的分量如何表示?

方式1:直接用   test.data=‘a’;  test.next=q;

方式2:先让指针变量p指向该结点的首地址,然后用:    p->data='a';     p->next=q;  

方式3:先让指针变量p指向该结点的首地址,然后用:    (*p).data=‘a’; (*p).next=q;

链表的实现{线性链表,循环链表,双向链表}

1. 单链表的读取 (或修改)

难点:单链表中想取得第i个元素,必须从头指针出发寻找(顺藤摸瓜),不能随机存取 。 注意:判断i不合法的情况,和表空的情况。

思路:要修改第i个数据元素,关键是要先找到该结点的指针p,然后用p->data=new_value 即可。

Status GetElem_L(LinkList L, int i, ElemType &e){
    P=L->next;      
    j=1;    //j用来计数
    while(p && j<i)  {
    p=p->next;    ++j;
    }
    if (!p || j>i)
    return ERROR;
    //!p即p为空,有两种情况,第一是表为空,第二是i的值超过了表的长度,while循环执行完后p走到最后一个元素还没有找到i的位置。j >i 就是i是0或者负数的情况。
    e=p->data;
    return OK;
}
// 注意:L为带头结点的单链表的头指针 
// 当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR

2. 单链表的插入

在链表中插入一个元素的示意图如下:P指向要插入的位置的前面的那个元素。

元素x结点应预先生成: S=(ElemType*)malloc(m); S->data=x;

插入步骤(即核心语句):Step 1:s->next=p->next; Step 2:p->next=s ;

Status ListInsert_L(LinkList &L,int i,ElemType e){
    p=L; 
    j=0;
    while(p && j<i-1){
    p=p->next;
    ++j
    }//p将指向第i-1个元素
    if(!p || j>i-1) 
    return ERROR;
    s=(LinkList)malloc(sizeof(LNode));
    s->data=e; 
    s->next=p->next;
    p->next=s;
    return OK;
}

3. 单链表的删除

删除步骤(即核心语句):

q = p->next;           //保存b的指针,靠它才能指向c
p->next=q->next;  //a、c两结点相连
free(q) ;               //删除b结点,彻底释放
Status ListDelete_L(LinkList &L,int i,ElemType &e){
    p=L; j=0;
    while(p->next && j<i-1){
    p=p->next;++j
    }
    if(!p->next || j>i-1) 
    return ERROR;
    //p指向第i-1个结点
    //判断p->next ,是要保证p的后面还有结点。
    q=p->next; 
    e=q->data; 
    p->next=q->next;
    free(q)
    return OK;
}

4. 单链表的建立和输出

实现思路:先开辟头指针,然后陆续为每个数据元素开辟存储空间并赋值,并及时将地址送给前面的指针。 实现方法:头插法和尾插法。

头插法:每次都插入到头结点的后面,即链表的最前面。

Void CreateList_L (LinkList &L, int n) {
  //逆位序输入n个元素的值,建立带表头结点的单链线性表L.
   L = (LinkList) malloc (sizeof(LNode));
   L→next = NULL;        // 先建立一个带头结点的单链表
   for ( i =n; i>0; --i) {
        p = (LinkList) malloc (sizeof(LNode)); // 生成新结点
        scanf (&p →data);       // 输入元素值
        p→next = L→next ;  
        L→next = p;              // 插入到表头
    }
}  // CreateList_L

尾插法:要有一个指针总是指向最后一个元素

Void CreateList_L (LinkList &L, int n) {
  //正序输入n个元素的值,建立带表头结点的单链线性表L.
   L = (LinkList) malloc (sizeof(LNode));
   L→next = NULL;        // 先建立一个带头结点的单链表
  q=L;//q总是指向最后一个元素
   for ( i =n; i>0; --i) {
        p = (LinkList) malloc (sizeof(LNode)); //新结点
        scanf (&p →data);        p->next=null; // 输入元素值
        q→next = p;          //把新结点p放到q的后面
        q=q→next ;              
    }
}  // CreateList_L

5. 应用举例

1、设计算法实现单链表的遍历。

void travle(LinkList L){
	LinkList p
	cout<<"建立的链表为:";
	for(p=L->next;p!=NULL;p=p->next)
		cout<<p->data<<"  ";
}

2、求单链表的长度。

void len_LL (LinkList L,int &len){
	LinkList p;
	for(p=L->next;p!=NULL;p=p->next)
	len++;
}

3、将单链表逆置。

提示:参考头插法,从头依次读取链表上的结点,每次读取的结点都插入到一个新链表的头部。

4、两个链表的归并

已知:线性表 A、B,分别由单链表 LA , LB 存储,其中数据元素按值非递减有序排列

要求:将 A ,B 归并为一个新的线性表C , C 的数据元素仍按值非递减排列  。设线性表 C 由单链表 LC 存储。

假设:A=(3,5,8,11),B=(2,6,8,9,11)

预测:合并后 C =( 2 , 3 ,  5 , 6 , 8 , 8 , 9 , 11,11 )

                                           

算法分析:算法主要包括:搜索、比较、插入三个操作:

搜索:需要两个指针搜索两个链表;

比较:比较结点数据域中数据的大小;

插入:将两个结点中数据小的结点插入新链表

Void   MergeList_L(LinkList  &La,LinkList &Lb,LinkList &Lc){
    //按值排序的单链表LA,LB,归并为LC后也按值排序
    pa=La->next;  pb=Lb->next;   
    Lc=La;pc=La    //用La的头结点,作为Lc的头结点  
    while(pa&&pb){
        //将pa 、pb结点按大小依次插入C中
        if(pa->data<=pb->data){
        pc->next=pa; 
        pc=pa;  
        pa=pa->next;
        }
        else {
        pc->next=pb;  
        pc=pb;  
        pb=pb->next
        }  
    }
    pc->next = pa?pa:pb ;     //插入剩余段
    free(Lb);        //释放Lb的头结点
} //MergeList_L

可以看出:归并两个链表时,不需要另建新的 存储空间,只需将原来链表中的节点之间的关 系解除,重新安排顺序。

6. 其它链表形式

用一维数组存放链表,只要定义一个结构类型(含数据域指示域)数组,就可以完全描述链表,这种链表称为静态链表

注:数据域含义与前面相同,指示域相当于前面的指针域。

静态链表的插入与删除操作与普通链表一样,不需要移动元素,只需修改指示器就可以了。

静态链表

#define MAXSIZE  1000
typedef struct{
     ElemType data;
     int cur;
}component,SLinkList[MAXSIZE]

链表首尾相连,只要将表中最后一个结点的指针域指向头结点即可 (P->next=head;) 。这种形成环路的链表称为循环链表

循环链表:表中最后一个结点的指针域指向头结点,整个链表形成一个环。

循环链表的实现无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。从任一结点出发均可找到表中其他结点。

循环链表的操作与线性链表基本一致,差别在于算法中的循环条件不是p或p->next是否为空,而是它们是否等于头指针。

单链表查找直接前驱,只要把单链表再多开一个指针域即可(例如用*next和*prior;) 。

这种有两个指针的链表称为双向链表。其特点是可以双向查找表中结点。

特别:带头结点的空双向链表样式:

双向链表在非线性结构(如树结构)中将大量使用。

双向链表:结点有两个指针域,一个指向直接前驱,一个指向直接后继。

克服了单链表的单向性(查后继O(1),查前驱O(n))的缺点。

双链表是由头指针head惟一决定的。

双链表进行插入和删除操作时必须要同时修改两个方向的指针。

Status ListInsert_DuL(DuLinkList &L,int i, ElemType e){
    if(!(p=GetElemP_DuL(L,i))) return ERROR;
    if(!(s=(DuLinkList)malloc(sizeof(DulNode)))) return ERROR;
    s->data=e;
    s->prior=p->prior;        p->prior->next=s;
    s->next=p;                p->prior=s;
    return Ok;
}

Status ListDelete_DuL(DuLinkList &L,int i, ElemType &e){
    if(!(p=GetElemP_DuL(L,i))) return ERROR;
    e=s->data;
    p->prior->next=p->next;
    p->next->prior= p->prior;
    //两行的顺序是否可以颠倒?
    free(p); return Ok;
}

链表的运算效率分析

时间效率分析

1. 查找  因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)

2. 插入和删除    因线性链表不需要移动元素,在给出某个合适位置的指什后,插入和删除操作所需的时间仅为 O(1)。   

但是,如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为 O(n)。

空间效率分析

链表中每个结点都要增加一个指针空间,相当于总共增加了n 个整型变量,空间复杂度为 O(n)

 

顺序存储和链式存储各有哪些优缺点?

顺序存储的优点是存储密度大(=1),存储空间利用率高。缺点是插入或删除元素时不方便。    

链式存储的优点是插入或删除元素时很方便,使用灵活。缺点是存储密度小(<1),存储空间利用率低。

事实上,链表插入、删除运算的快慢是以空间代价来换取时间。

存储密度=结点数据本身所占的存储量/结点结构所占的存储总量

如何选择?

顺序表适宜于做查找这样的静态操作;

链表宜于做插入、删除这样的动态操作。

若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;

若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表。

 

顺序表

链表

基于空间考虑

分配方式

静态分配。程序执行之前必须明确规定存储规模,若线性表长度n变化较大,则存储规模难于预先确定,估计过大将造成空间浪费,估计过小又将使空间溢出机会增多。 

动态分配。只要内存空间尚有空闲就不会产生溢出,因此当线性表的长度变化较大,难以估计其存储规模时,以采用动态链表作为存储结构为好。

存储密度

=1.当线性表的长度变化不大,易于事先估计其大小时,为了节约存储空间,宜采用顺序存储作为存储结构。

<1

基于时间考虑

存取方法

随机存取结构。对表中任一结点都可在O(1)时间内直接取得。若线性表的操作主要是进行查找,很少做插入和删除操作,则采用顺序表作为存储结构为宜。

顺序存取结构。链表中的结点需从头指针起顺着链扫描才能取得。

插入删除操作

在顺序表中进行插入和删除操作,平均要移动表中一半的结点,尤其是每个结点的信息量较大时,移动结点的开销就相当可观。

在链表中的任何位置插入和删除,都只需要修改指针。对于频繁进行插入和删除操作的线性表,宜采用链表作存储结构。若表的插入和删除主要发生表的首尾两端,则采用尾指针表示的单循环链表为宜。

代码实现:链表的基本操作https://blog.csdn.net/qq1457346557/article/details/107122832

 

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值