数据结构与算法-链表(线性表的链表表示和实现)

目录

单链表的定义和表示

什么是链表

单链表的基本操作的实现 

1、单链表的初始化

2、单链表的取值

3、单链表的按值查找

 

 4、单链表的插入操作

5、单链表的删除元素操作

6、数组和单链表的效率PK

 

7、单链表的整表创建 

8、单链表的整表删除 

 

循环链表 

双向链表

1、双向链表的描述:

2、双向链表的插入

3、双向链表的删除 

顺序表和链表的比较

1、空间性能的比较

2、时间性能的比较 

链表与数组的主要区别

数组的优点

数组的缺点

链表的优点

链表的缺点


本文部分内容摘自极客时间《数据结构与算法之美》和网络,仅供笔者学习和复习用。

单链表的定义和表示

什么是链表

1.和数组一样,链表也是一种线性表。
2.从内存结构来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构。

一个链表有很多个节点,各个节点之间通过指针连接起来,所以各个结点之间的位置可以不连续,也就是可以放在不同的位置,所以在空间上可以是不连续的;但对于一个节点,因为节点内部是一个整体,所以就要占用连续的存储空间。

各个节点在链表中都是一个指针变量,也就是一个32位的字节。每一个指针变量都要分配内存,而这些指针变量的内存地址是连续的。每个指针所指向的地址可以是连续的,当然也可以不是连续的,这个顺序链表有着本质的区别。

3.链表中的每一个内存块被称为节点Node。节点除了存储数据外,还需记录链上下一个节点的地址,即后继指针next。 

4.每个节点包括两个域:其中存储数据元素的域称为数据域,存储直接侯冀存储位置的域称为指针域. 指针域存储的信息称作指针链。

5.n个节点(ai(1<=i<=n)的存储映像)链结成一个链表,即为线性表:

(a1,a2,a3,···an)

的链式存储结构。又由于此链表的节点中只包含一个指针域,故又称为线性链表单链表

 单链表可由头指针唯一确定,在C语言中可以用“结构指针”来描述:

//单链表的存储结构
typedef struct LNode {
    ElemType data ;        //节点的数据域
    struct LNode *next;    //节点的指针域
}LNode ,*LinkList;         //LinkList为指向结构体LNode的指针类型

 为了提高程序的可阅读性,在此对同一结构体指针类型起了两个名称,LinkList与LNode*,两者本质上是相等的,通常习惯用LinkList定义单链表,强调定义某个单链表的头指针;用LNode*定义指向单链表的任意节点的指针变量。

下面对  首元结点、头结点、头指针三个容易混淆的概念加以说明:

  1. 首元结点是指链表中存储第一个数据元素a1的结点。
  2. 头结点是在首元节点前面预设的一个结点,其指针域指向首元结点。头结点的数据域可以不存储任何数据元素,也可存储与数据元素类型相同的其他附加信息。例如,当数据元素为整型时,头结点的数据域 可以存储该线性表的长度。
  3. 头指针是指向链表中第一个结点的指针。若链表没有头结点,则头指针所指向的节点为线性表的头结点;若链表不设头结点,则头指针所指向的结点为该线性表的首元结点。

单链表的基本操作的实现 

1、单链表的初始化

Status InitList (LinkList &L ) {
    L = new LNode ;             //生成新的节点作为头结点,用头指针L指向头结点
    L->next  = Null;            //头结点的指针置空
    return OK;
}

2、单链表的取值

Statua GetElem (LinkList L,int i ,ElemType &e) {
    //再带头结点的单链表L中根据序号i获取元素的值,用e返回第i个元素中的值
    p = L->next;j = 1;       //初始化,p指向首元结点,计数器j初始值记为1
    while (p&&j<i) {         //顺序向后遍历,直到p为空或者p指向第i个元素
        p = p->next;
        ++j;
    }
    if (!p||j>i) {           //i值不合法
        return ERROR;
    }
    e = p->data;             //取第i个结点的数据域
    return OK;
}

3、单链表的按值查找

LNode *LocateElem (LinkList L,ElemType e) {
    p = L->next;                         //初始化,p指向首元结点
    while (p && p->data !=e) {           //顺着链域向后扫描,直到p为空或者p所指向的结点的数据域等于e
    p = p ->next;                        //p指向下一个结点
    }
    return p;                            //擦汗信号成功返回值为e的结点的地址,查找失败p为NULL
}

 

 4、单链表的插入操作

Status ListInsert (LinkList L, int i,ElemType e){
    //在带头结点的单链表L中第i个位置插入值为e的新结点
    p = L; j = 0;                 
    while (p && (j < i-1)) {      
    p = p->next;                  //查找第i-1个结点,p指向该结点             
    ++j;
    }
    if (!p || j > i-1) {         //i>n+1或者i<1  
    return ERROR;
    }
    s = new LNde;                //生成新结点
    //s = (LinkList)malloc(sizeof(Node));
    s ->data = e;                //将结点*是、的数据域置为e
    s ->next = p ->next;         //新结点s的指针域指向结点ai
    p ->next = s;                //把结点*p的指针域指向新结点*s
    return OK;
}

当然了关于插入操作的话这有一个常考点:

上面插入操作代码中的

 s ->next = p ->next;      

 p ->next = s;              

两者顺序是否可以倒过来? 

如果先执行p ->next = s的话,s的地址就覆盖了p ->next的地址,那么接下来在执行s ->next = p ->next的话就会发现s的next又指向了前面的p ->next;这样一来s的next又指向了自己,所以两者的顺序不能交换!

5、单链表的删除元素操作

Status ListDelete (LinkList *L,int i ,ElemType *e) {
    //删除第i个结点,并把第i个结点的数据域用*e接受
    int j ;
    LinkList p ,  q;

    p = *L;
    j = 1;

    while (p ->next && j<i) {  //类似插入操作一样先从链表第一位开始遍历,直到找到i为止
        p = p ->next;
        ++j;
    }
    if (!p || j>i) {           //如果为空表或者到达了表的末尾,返回错误
        return ERROR;
    }
    q = p ->next;              //把要删除节点的指针域给q
    p ->next = q ->next;       //然后把q的next指向原来p的next指向的结点
    
    *e = p ->data;             //接收删除元素
    free(q);                   //释放内存
    return OK;
}

6、数组和单链表的效率PK

学了单链表的插入和删除操作后,我发现两者无论是删除还是插入,它们无非都包含两部分:

  • 第一部分就是遍历查找第i个元素;
  • 第二部分就是实现插入和删除;

从整个算法来说,我们很容易的推出它们的时间复杂度都是O(n);

详细来说啊,只要你不知道第i个元素的具体位置,你就得遍历查找她。

那么不就是说这个单链表数据结构在插入和删除操作上和线性表的顺序存储结构相比没啥太大的优势了么?

仔细一想啊,这个单链表还真有它存在的道理啊 

在某些特殊的情况下,比如我想在第i个位置后面连续地插入10个、100个、1000个甚至更多元素的时候:

  • 顺序存储的数组马上就瓦了,因为它每一次插入和删除一个元素的时候,后面的朋友们都要移动n-1个位置,所以每次都是O(n);
  • 单链表的话只需要找到第i个元素的位置的指针,此时为O(n),接下来只需要通过简单的赋值和移动指针而已,时间复杂度都是O(1)拍手.gif。
  • 显然对于插入和删除操作月平凡的操作,单链表的效率和优势就是越明显啦~

 

7、单链表的整表创建 

      头插法(前插法)建立单链表

#include <iostream>
using namespace std; 
void CreateList_H (LinkList &L, int n) {
    L = new LNode;
    L ->next = NUll;                //创建一个带头结点的空链表
    
    for (i = 0;i<n;++i) {
    p = new LNode;
    cin>>p ->data;                 //生成新结点p
    p ->next = L ->next;           //把原先头结点指针域指向的结点用新节点p指向
    L ->next = p;                  //把新结点p查到头结点后面
    }
}

   

     尾插法(后插法)建立单链表

#include <iostream>
using namespace std; 
void CreateList_R (LinkList &L, int n) {
    L = new LNode;
    L ->next = NUll;                //创建一个带头节点的空链表
    r = L;                          //尾指针r指向头节点
    
    for (i = 0;i<n;++i) {
    p = new LNode;
    cin>>p ->data;                 //生成新节点p
    p ->next = NULL;               //把新节点p的指针指向NULL
    r ->next = p;                  //把原先的尾指针指向新节点
    r = p;                         //r指向新的尾节点p
    }
}

8、单链表的整表删除 

Status ClearList (LinkList *L) {
    LinkList p, q;

    p = (*L) ->next;        //把p设为首元结点
    while (p) {             
        q = p ->next;       //先把p的指针域丢给q
        free (p);           //再把p扔掉
        p = q;              //最后把刚才的q作为新p
    }
    (*L) ->next = null;     //完事儿后记得把*L指向NULL
    return OK;
}

 

循环链表 

循环链表是另一种形式的链式存储结构,它的特点是最后一个元素的指针域指向头结点,整个链表形成一个环。那么无论从那个结点开始都可以找到其他结点。

循环链表的操作和单链表的大同小异,但是当遍历链表的时候,判断当前指针是否指向尾结点的终止条件不同。在单链表中的条件是:p!=NUll或者p ->next !=NULL,循环单链表的终止条件不是p !=L 或者 p ->next !=L.

双向链表

以上讨论的链式存储结构的结点中只有一个指示 直接后继的指针域,由此,从某个结点出发只能顺指针向后寻查其他结点。若要寻查结点的直接前驱,则必须从表头指针出发。换句话说,在单链表中,查找直接后继结点的执行时间为O(1),而查找直接前驱的执行时间为O(n)。为克服单链表这种单向性的缺点,可利用双向链表( Double Linked List )。
顾名思义,在双向链表的结点中有两个指针域,一个指向直接后继,另-个指向直接前驱。

1、双向链表的描述:

typedef struct DuLNode {
    ElemType data ;           //当然得有数据域
    struct DuLNode *prior;    //指向前驱的指针域
    struct DuLNode *next;     //指向后驱的指针域
}DuLNode,*DuLinkList;

2、双向链表的插入

Status ListInsert_Dul (DuLinkList &L, int i ,ElemType e) {
    //在带头结点的双向链表L的第i个元素前面插入元素e
    if (!(p = GetElem (L,i)))             //在L中确定第i个元素的位置指针
        return ERROR;                     //p为NULL时,第i个元素不存在
    s = new DuLNode ;                     //新建结点
    s ->data = e;                         //新结点赋值
    s ->prior = p ->prior;                //把原先结点的前指针留给新结点
    p ->prior ->next = s;                 //原先结点的前驱结点的指针域指向新结点s
    s ->next = p;                         //把p塞到新结点s的后面
    p ->prior = s;                        //原先结点的前指针指向新结点s
    return OK;
}

3、双向链表的删除 

Status ListInsert_Dul (DuLinkList &L, int i) {
    //删除带头结点的双向链表L的第i个元素
    if (!(p = GetElem (L,i)))             //在L中确定第i个元素的位置指针
        return ERROR;                     //p为NULL时,第i个元素不存在
    p ->prior ->next = p ->next;          //p结点的前驱结点的后继指针指向结点p的下一个结点
    p ->next ->prior = p ->prior;         //p节点的后继结点的前驱指针指向p结点的前驱结点
    Delete p;                             //释放p
    return OK;
}

顺序表和链表的比较

1、空间性能的比较

  • 顺序表的存储空间必须预先分配,元素个数扩充受到限制,容易造成空间浪费或者溢出。
  • 单链表采用链式存储结构,可以被空间中任意角落的元素串联起来,只要空间允许,链表中的元素个数就没有限制。

2、时间性能的比较 

查找

  • 顺序存储结构O(1);
  • 单链表O(n);

插入、删除和随机访问的时间复杂度

  • 数组:插入、删除的时间复杂度是O(n),随机访问的时间复杂度是O(1)。
  • 链表:插入、删除的时间复杂度是O(1),随机访问的时间复杂端是O(n)。

链表与数组的主要区别

  1. 数组的元素个数是固定的,而组成链表的结点个数可按需要增减;
  2. 数组元素的存诸单元在数组定义时分配,链表结点的存储单元在程序执行时动态向系统申请:
  3. 数组中的元素顺序关系由元素在数组中的位置(即下标)确定,链表中的结点顺序关系由结点所包含的指针来体现。
  4. 对于不是固定长度的列表,用可能最大长度的数组来描述,会浪费许多内存空间。
  5. 对于元素的插人、删除操作非常频繁的列表处理场合,用数组表示列表也是不适宜的。若用链表实现,会使程序结构清晰,处理的方法也较为简便。

数组的优点

  • 随机访问性强
  • 查找速度快

数组的缺点

  • 插入和删除效率低
  • 可能浪费内存
  • 内存空间要求高,必须有足够的连续内存空间。
  • 数组大小固定,不能动态拓展

链表的优点

  • 插入删除速度快
  • 内存利用率高,不会浪费内存
  • 大小没有固定,拓展很灵活。

链表的缺点

  • 不能随机查找,必须从第一个开始遍历,查找效率低

猜你喜欢

转载自blog.csdn.net/qq_44957186/article/details/104350819