王道数据结构 第二章 线性表(2)

线性表的链式表示

顺序表达插入删除操作需要移动大量元素,影响了运行效率,故而引出了线性表的链式存储。
在使用链式存储的过程中不需要使用地址连续的存储单元,不要求逻辑上相邻的两个元素在物理上也相邻。用过“链”建立起数据元素之间的逻辑关系。对线性表的插入删除不需要移动元素,只需要修改指针。

单链表的定义

结点类型的描述如下:

typedef struct Lnode {
    ElemType data;
    struct Lnode *next;
}Lnode,*LinkList;

利用单链表可以解决顺序表需要大量的连续存储空间的缺点,但是单链表附加指针域,也存在浪费存储空间的缺点。单链表是非随机存取的数据结构,不能直接找到表中特定的结点。查找某一个特定结点时,需要从表头开始遍历,依次查找。
通常用头指针来标识一个单链表,头指针为NULL时表示一个空表。
为了操作上的方便,在单链表的第一个结点之前附加一个结点,成为头结点。头结点的数据域可以不设任何信息,也可以记录表长等相关信息。头结点的指针域指向线性表的第一个元素结点。

头结点和头指针的区别:不管带不带头结点,头指针始终指向链表的第一个结点,而头结点是带头结点链表中的第一个结点,结点内不存储信息。

引入头结点的好处:

  1. 由于开始结点的位置被存放在头结点的指针域中,所以链表的第一个位置上的操作和在表的其他位置上的操作一致,不需要进行特殊处理。
  2. 无论链表是否为空,其头指针是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也统一了。

单链表各种操作省略,后续习题会有。

双链表

在单链表中,访问后继结点的时间复杂度为O(1),访问前驱结点的复杂度为O(n)。为了克服这点,引入了双链表。双链表结点中有两个指针,prior和next,表示前驱和后继。
结点类型的描述如下:

typedef struct DNode {
    ElemType data;
    struct Lnode *next,*prior;
}Dnode,*DLinkList;

双链表中执行按值和按位查找的操作和单链表相同。
双链表在插入和删除操作的实现上和单链表有很大不同,因为链变化时也要对前驱指针作出修改,要保证在修改的过程中不断链。
插入和删除结点算法的时间复杂度为O(1)

在p所指的结点后,插入结点*s
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
双链表的删除操作

删除结点p的后继结点q

p->next = q->next;
q->next->prior = p;
free(q);

循环链表

循环链表和单链表的区别在于,最后一个结点的指针不为NULL,指向头结点。
循环链表判空条件就不是原来的判断头结点指针是否为空,而是判断它是否等于头指针。
循环链表的插入删除算法与单链表几乎一样,不同的是若在表尾进行,执行的操作不同。

循环链表可以从表中任意位置开始遍历链表。
若对于链表的操作是在表头和表尾进行的,可对循环链表不设置头指针而仅设置尾指针,这样操作效率更高。因为如果设置为头指针,需要花O(n)才能进行表尾的操作,而如果设置的是尾指针r,可通过r->next找到头指针,对表头和表尾的操作的复杂度都为O(n).

循环双链表

在循环双链表中,某结点*p为尾结点时,p->next = L
若循环双链表为空表时,其头结点的prior和next 都为L

静态链表

静态链表是借助数组来描述线性表的链式存储结构,结点也有data和next。
这里的指针是结点的相对地址(数组下标),又称游标。
和顺序表一样,静态链表也要提前分配一块连续的内存空间。
结构类型的描述如下

#define MAXSIZE 50
typedef struct {
    ElemType data;
    int next;
} sLinkList[MAXSIZE];

静态链表以next为-1作为结束的标志。插入删除操作与动态链表相同,只需要修改指针而不需要移动元素。
使用起来不够方便。

顺序表和链表的比较(重点)

  1. 存取方式
    顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。

  2. 逻辑结构与物理结构
    采用顺序存储时,逻辑上相邻的元素,其对应的物理存储位置也相邻。
    链式存储时,逻辑上相邻的元素,其物理存储位置不一定相邻,其对应的逻辑关系是通过指针链接来表示的。

  3. 查找插入和删除操作
    对于按值查找,当顺序表无序的情况下,两者都为O(n)。
    当顺序表有序时,可以采用二分查找,时间复杂度为O(logn)。
    对于按序号查找,顺序表支持随机访问,时间复杂度仅为O(1)。链表的平均时间复杂度为O(n).
    顺序表的插入删除平均需要移动半个表长的元素。
    链表的插入删除仅需要修改相关结点的指针域。
    链表每个结点都带有指针域,故存储空间消耗大于顺序表,存储密度不够大。

  4. 空间分配
    顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新的元素就造成内存溢出,需要预先分配足够大的存储空间。预先分配过大,会导致顺序表后部大量闲置。分配过小,会造成溢出。
    动态存储分配虽然存储空间可以扩充,但是需要移动大量元素,倒置操作效率降低,若内存中没有更大块的连续存储空间将导致分配失败。
    链式存储的结点空间只在需要的时候申请分配,只要有内存空间就可分配,操作灵活高效。

实际中如何选择存储结构?

  1. 基于存储的考虑
    对线性表长度或存储规模难以估计,不宜采用顺序表。
    链表不用事先估计存储规模,但是存储密度低,小于1
  2. 基于运算的考虑
    若常用运算是按序号访问数据元素,顺序表优于链表。
    若常用运算是插入删除,链表由于顺序表。
  3. 基于环境的考虑
    顺序表实现简单,链表复杂。

下面是习题里不熟悉的点

  1. 单链表中,增加一个头结点的目的是为了方便运算的实现
  2. 对于一个头指针为head的带头结点的单链表,判定该表为空表的条件是head->next = NULL,对于不带头结点的单链表,判定为空的条件为head = NULL
  3. 一个链表最常用的操作是在末尾插入结点和删除结点,则选用(带头结点的双循环链表)最节省时间。
  4. 需要分配较大的空间,插入和删除不需要移动元素的线性表是(静态链表)。

编程题将在下一部分列出。

    原文作者:球球球球笨
    原文地址: https://www.jianshu.com/p/b9472c92649a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞