程序猿必修课之数据结构(四)线性表2

原文出自:http://www.jianshu.com/p/94fc4be7d61e

你还在为开发中频繁切换环境打包而烦恼吗?快来试试 Environment Switcher 吧!使用它可以在app运行时一键切换环境,而且还支持其他贴心小功能,有了它妈妈再也不用担心频繁环境切换了。https://github.com/CodeXiaoMai/EnvironmentSwitcher

上一章:程序猿必修课之数据结构(三)线性表1

上篇我们复习的线性表的顺序存储结构,它的最大缺点就是:插入和删除是需要移动大量元素,造成时间的浪费。

导致这个问题的原因是,相邻两个元素的存储位置也具有邻居关系,也就是说它们在内存中是挨着的,中间没有空隙,当然就无法快速插入,而删除后,当中就会留出空隙,自然需要弥补。链式存储就是为了解决这个问题而产生的。

线性表链式存储结构定义

线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些元素可以存在内存未被占用的任意位置。

在顺序结构中,每个数据元素只需要存储数据元素信息就可以了。现在链式结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址。

为了表示每个元素 Ai 与其直接后继元素 A(i+1) 之间的逻辑关系,对数据元素 Ai 来说,除了存储其本身的信息外,还需要存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息叫做指针或链。这两部分信息组成数据元素 Ai 的存储映像,称为结点(Node)。

n 个结点链接成一个链表,即为线性表的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。

链表中第一个结点的存储位置叫做头指针。

为了方便对链表进行操作,会在单链表的第一个结点前附加一个结点,称为头结点。头结点的数据域可以不存储任何信息,也可以存储线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。

头指针与头结点的异同

头指针:

  • 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
  • 头指针具有标识作用,所以常用头指针冠以链表的名字。
  • 无论链表是否为空,头指针均不为空。头指针是链表的必要元素。

头结点:

  • 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(也可存放链表的长度)。
  • 有了头结点,对在第一个元素结点前插入结点或删除第一个结点,其操作与其它结点的操作就统一了。
  • 头结点不一定是链表必须元素。

线性表链式存储结构代码描述

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

单链表的读取

在线性表的顺序储存结构中,我们要计算任意一个元素的存储位置是很容易的。但是在单链表中,由于第 i 个元素到底在哪,一开始不知道,必须从头开始找。

获取单链表第 i 个数据的算法步骤

  1. 声明一个结点 p 指向链表第一个结点,初始化 j 从 1 开始;
  2. 当 j < i 时,就遍历链表,让 p 的指针向后移动,不断指向下一结点, j 累加 1;
  3. 若到链表末尾 p 为空,则说明第 i 个元素不存在;
  4. 否则查找成功,返回结点 p 的数据。

代码如下:

ElemType GetElem (LinkList L, int i) {
    int j;
    LinkList p;
    p = L->next;
    j = 1;
    while (NULL != p && j < i) {
        p = p->next;
        ++j;
    }
    if (!p || j > i)
        return Error;
    return p->data;
}

由于这个算法的时间复杂度取决于 i 的位置,当 i = 1 时,则不需遍历,而当 i = n 时则遍历 n - 1次才可以,因此最坏情况的复杂度为O(n)。

单链表的插入

单链表第 i 个位置插入结点的算法步骤:

  1. 声明一个结点 p 指向链表第一个结点,初始化 j 从 1 开始;
  2. 当 j < i 时,就遍历链表,让 p 的指针向后移动,不断指向下一个结点, j 累加 1;
  3. 若到链表末尾 p 为空,则说明第 i 个位置不存在;
  4. 否则查找成功,创建一个空结点 s;
  5. 将数据元素赋值给 s->data;
  6. 单链表的插入标准语句 s->next = p->next; p->next = s;
  7. 返回成功。

代码如下:

bool ListInsert (LinkList *L, int i, ElemType e) {
    int j;
    LinkList p, s;
    p = *L;
    j = 1;
    while (p && j < i) {
        p = p->next;
        ++j;
    }
    if (!p || j > i)
        return false;
    s = (LinkList)malloc(sizeof(Node));
    s->data = e;
    s->next = p->next;
    p->next = s;
    return true;
}

单链表删除第 i 个结点的算法步骤:

  1. 声明一个结点 p 指向链表第一个结点,初始化 j 从 1 开始;
  2. 当 j < i 时,就遍历链表,让 p 的指针向后移动,不断指向下一个结点, j 累加 1;
  3. 若到链表末尾 p 为空,则说明第 i 个结点不存在;
  4. 否则查找成功,将要删除的结点 p->next 赋值给 q;
  5. 单链表的删除标准语句 p->next = q->next;
  6. 将 q 结点的数据赋值给 e,作为返回;
  7. 释放 q 结点;
  8. 返回成功。

代码如下:

bool ListDelete (LinkList *L, int i, ElemType *e) {
    LinkedList p, q;
    int j;
    p = *L;
    j = 1;
    while (p->next && j < i) {
        p = p->next;
        j++;
    }
    if (!(p->next) || j > i)
        return false;
    q = p->next;
    p->next = q->next;
    *e = q->data;
    free(q);
    return true;
}

对于插入或删除数据越频繁的操作,单链表的效率比顺序存储结构要高。

单链表的创建

我们已经知道,顺序存储结构的创建,其实就是一个数组的初始化,即声明一个类型和大小的数组并赋值的过程。而单链表和顺序存储结构不一样,它不像顺序存储结构这么集中,可以很散,是一种动态结构。对于每个链表来说,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。

所以创建单链表的过程就是一个动态生成链表的过程,即从“空表”的初始状态,依次建立各元素结点,并逐个插入链表。

创建单链表整表的步骤(头插法):

  1. 声明一个结点 p 和计数器变量 i;
  2. 初始化一个空链表 L;
  3. 让 L 的头结点的指针指向 NULL,即建立一个带头结点的单链表;
  4. 循环:
    1. 生成一个新结点赋值给 p;
    2. 将值赋值给 p的数据域 p->data;
    3. 将 p 插入到头结构与前一新结点之间。

代码如下:

void createList(LinkList *L, int n) {
    LinkList p;
    int i;
    /* 初始化随机数种子 */
    srand (time(0));
    *L = (LinkList)malloc(sizeof(Node));
    /* 创建一个带头结点的单链表 */
    (*L)->next = NULL;
    for (i = 0; i < n; i++) {
        p = (LinkList)malloc(sizeof(Node));
        /* 随机生成100以内的数字 */
        p->data = rand() % 100 + 1;
        p->next = (*L)->next;
        (*L)->next = p;
    }
}

尾插法:

void createList(LinkList *L, int n) {
    LinkList p, r;
    int i;
    srand(time(0));
    *L = (LinkList)malloc(sizeof(Node));
    (*L)->next = NULL;
    /* 指向尾结点的结点 */
    r = *L;
    for (i = 0; i < n; i++) {
        p = (Node *)malloc(sizeof(Node));
        p->data = rand() % 100 + 1;
        /* 将表尾结点的指针指向新结点 */
        r->next = p;
        /* 将当前的新结点定义为尾结点 */
        r = p;
    }
    r->next = NULL;
}

单链表的整表删除

单链表整表删除的步骤:

  1. 声明一个结点 p 和 q;
  2. 将第一个结点赋值给 p;
  3. 循环:
    1. 将下一个结点赋值给 q;
    2. 释放 p;
    3. 将 q 赋值给 p。

代码如下:

bool clearList(LinkList *L) {
    LinkList p, q;
    /* p指向第一个结点 */
    p = (*L)->next;
    while(p) {
        q = p->next;
        free(p);
        p = q;
    }
    (*L)->next = NULL;
    return true;
}

单链表结构与顺序存储结构优缺点

比较内容 顺序存储 单链表
存储分配方式 用一段连续的存储单元依次存储线性表的数据元素 采用链式存储结构,用一组任意的存储单元存放线性表的元素
查找的时间性能 O(1) O(n)
插入删除的时间性能 O(n) O(1)
空间性能 需要预先分配存储空间,分大了浪费,分小了易发生上溢 不需要分配存储空间,只要有就可以分配,元素个数不受限制

总结

  • 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。
  • 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。

总之,线性表的顺序存储结构和单链表结构各有优缺点,不能简单的说哪个好,哪个不好,需要根据实际情况综合平衡采用哪种数据结构更能满足和达到需求。

下一章:程序猿必修课之数据结构(五)线性表3

你可能感兴趣的:(程序猿必修课之数据结构(四)线性表2)