数据结构(二):线性表

222 阅读7分钟

线性表的定义和基本操作

线性表的定义

  1. 线性表是具有相同数据类型的 n 个数据元素的有限序列,其中 n 为表长,n=0 时是一个空表
  2. 线性表的逻辑特性:
    • 除表头元素外,每一个元素都有且仅有一个直接前驱
    • 除表尾元素外,每一个元素都有且仅有一个直接后继
  3. 线性表的特点如下:
    • 表中元素个数有限
    • 表中元素具有逻辑上的顺序性,即表中元素有先后次序
    • 表中元素都是数据元素,每个元素都是单个元素
    • 表中元素的数据类型相同,即每个元素占有相同大小的存储空间
    • 表中元素具有抽象性,即只讨论元素之间的逻辑关系而不考虑元素的具体内容
  4. 注意,线性表是一种逻辑结构,表示元素之间一对一的相邻关系,而顺序表和链表指是指存储结构

线性表的基本操作

  1. InitList(&L):初始化表,构造一个空的线性表
  2. Length(L):求表长,返回表的长度,即 L 中元素的个数
  3. LocateElem(L, e):按值查找,在表中查找具有给定关键字值的元素
  4. GetElem(L, i):按位查找,查找在 L 中第 i 个位置上的元素
  5. ListInsert(&L, i, e):插入操作,在 L 中的第 i 个位置上插入元素 e
  6. ListDelete(&L, i, &e):删除操作,删除 L 中的第 i 个位置上的元素,并用 e 返回元素值
  7. PrintList(L):输出表
  8. Empty(L):判空操作,若 L 为空表则返回 true,若不为空表则返回 false
  9. DestroyList(&L):销毁操作,释放 L 所占用的内存空间
InitList(&L):初始化表,构造一个空的线性表
Length(L):求表长,返回表的长度,即 L 中元素的个数
LocateElem(L, e):按值查找,在表中查找具有给定关键字值的元素
GetElem(L, i):按位查找,查找在 L 中第 i 个位置上的元素
ListInsert(&L, i, e):插入操作,在 L 中的第 i 个位置上插入元素 e
ListDelete(&L, i, &e):删除操作,删除 L 中的第 i 个位置上的元素,并用 e 返回元素值
PrintList(L):输出表
Empty(L):判空操作,若 L 为空表则返回 true,若不为空表则返回 false
DestroyList(&L):销毁操作,释放 L 所占用的内存空间

线性表的顺序表示

顺序表的定义

  1. 线性表的顺序存储结构又称为顺序表

  2. 它的特点是:

    • 逻辑顺序与物理顺序相同,所以删除和插入操作需要移动大量元素
    • 可以实现随机存取,即通过首地址和元素与序号可在 O(1) 内找到指定的元素
    • 存储密度高,每个结点只存储数据元素
  3. 线性表的顺序存储结构描述为

    #define MaxSize 50 // 定义线性表的最大长度
    typedef struct {
        int data[MaxSize]; // 顺序表的元素
        int Length; // 顺序表的当前长度
    } SqList; // 顺序表的类型定义
    
  4. 数组可以静态分配,也可以动态分配,动态分配时要开辟一块更大的存储空间,来替换原来的存储空间

    #define MaxSize 50 // 表长度的初始定义
    typedef struct {
        int *data; // 动态分配数组的指针
        int MaxSize, length; // 数组的最大容量和当前个数
    } SeqList; // 动态分配数组顺序表的类型定义
    
  5. 动态分配语句

    L.data = (int *)malloc(sizeof(int)*InitSize); // C 语言的动态分配语句
    
    L.data = new int[InitSize]; // C++ 的动态分配语句
    
  6. 注意,动态分配并不是链式存储,它同样属于顺序存储结构,物理结构没有变化,依然是随机存取方式,只是分配的空间大小可以在运行时动态决定

顺序表的基本操作

  1. 插入操作

    • 在 L 上的第 i(1≤i≤L.length+1)个位置插入新元素 e

    • 代码如下:

      bool ListInsert(SqList &L, int i, int e) {
          if(i<1 || i>L.length+1) // 判断 i 的位置是否合法
              return false;
          if(L.length >= MaxSize) // 判断存储空间是否已满
              return false;
          for(int j=L.length; j>=i; j--) // 将第 i 个位置以及之后的元素都后移
              L.data[j] = L.data[j-1];
          L.data[i-1] = e; // 在第 i 个位置处插入元素 e
          L.length ++; // 线性表的长度加 1
          return true;
      }
      
    • 时间复杂度:O(n)

  2. 删除操作

    • 在 L 上的第 i(1≤i≤L.length)个位置删除元素,并用 e 返回

    • 代码如下:

      bool ListDelete(SqList &L, int i, int &e) {
          if(i<1 || i>L.length) // 判断 i 的位置是否合法
              return false;
          e = L.data[i-1]; // 将删除的元素赋值给 e
          for(int j=i; j<L.length; j++)  // 将第 i 个位置以及之后的元素都前移
              L.data[j-1] = L.data[j];
          L.length --;
          return true;
      } // 时间复杂度:O(n)
      
    • 时间复杂度:O(n)

  3. 按值查找(顺序查找)

    • 在 L 中查找第一个值为 e 的元素,并返回其次序

    • 代码如下:

      int LocateElem(SqList L, int e) {
          for(int i=0; i<L.length; i++)
              if(L.data[i] == e)
                  return i+1; // 若查找成功,返回位序 i+1
          return 0;
      } // 时间复杂度:O(n)
      
    • 时间复杂度:O(n)


线性表的链式表示

单链表的定义

单链表的结点类型描述如下:

typedef struct LNode { // 定义单链表结点类型
    int data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

单链表的基本操作

  1. 头插法建立单链表

    LinkList List_HeadInsert(LinkList &L) { // 逆向建立单链表
        L = (LinkList)malloc(sizeof(LNode)); // 创建头结点
        L->next = NULL; // 初始为空链表
        LNode *s;
        int x;
        scanf("%d", &x); // 输入结点的值
        while(x != 9999) { // 输入 9999 表示结束
            s = (LNode *)malloc(sizeof(LNode)); // 创建新结点
            s->data = x;
            s->next = L->next;
            L->next = s; // 将新结点插入 x 中,L 为头指针
            scanf("%d", &x);
        }
        return L;
    }
    
    • 头插法建立单链表时,读入数据的顺序与生成的链表的元素的顺序是相反的
    • 时间复杂度:O(n)
  2. 尾插法建立单链表

    LinkList List_TailInsert(LinkList &L) { // 正向建立单链表
        L = (LinkList)malloc(sizeof(LNode)); // 创建头结点
        LNode *s, *r=L; // r 为表尾指针
        int x;
        scanf("%d", &x); // 输入结点的值
        while(x != 9999) { // 输入 9999 表示结束
            s = (LNode *)malloc(sizeof(LNode)); // 创建新结点
            s->data = x;
            r->next = s; // r 指向新的表尾结点
            scanf("%d", &x);
        }
        r->next = NULL; // 尾结点置空
        return L;
    }
    
  3. 按位查找(查找第 i 个结点)

    LNode *GetElem(LinkList L, int i) {
        int j = 1; // 计数,初始为 1
        LNode *p = L->next; // 头结点指针赋给 p
        if(i == 0)
            return L; // 返回头结点
        if(i < 1)
            return NULL; // i 无效时返回 NULL
        while(p && j<i) { // 从第一个结点开始找,查找第 i 个结点
            p = p->next;
            j ++;
        }
        return p; // 返回第 i 个结点的指针,若 i 大于表长,则返回 NULL
    }
    
    • 时间复杂度:O(n)
  4. 插入结点

    p = GetElem(L, i-1); // 查找插入位置的前驱结点
    s->next = p->next;
    p->next = s;
    
    • 时间复杂度:O(n)

    • 前插操作

      s->next = p->next; // 修改指针域
      p->next = s;
      temp = p->data;
      p->data = s->data;
      s->data = temp;
      
  5. 删除结点

    p = GetElem(L, i-1); // 查找删除位置的前驱结点
    q = p->next; // q 指向被删除结点
    p->next = q->next; // 将 *q 结点从链中断开
    free(q); // 释放结点空间
    
    • 时间复杂度:O(n)

    • 删除操作的另一种方法

      q = p->next; // q 指向被删除结点
      p->data = p->next->data; // 和后继结点交换数据域
      p->next = q->next; // 将 *q 结点从链中断开
      free(q); // 释放结点空间
      

双链表

  1. 单链表只能从头依次顺序地向后遍历,双链表可以克服单链表的这个缺点

  2. 双链表的结点类型描述如下:

    typedef struct DNode{ // 定义双链表结点类型
        int data; // 数据域
        struct DNode *prior, *next; // 前驱和后继指针
    } DNode, *DLinkList;
    
  3. 双链表的插入操作

    s->next = p->next;
    p->next->prior = s;
    s->prior = p;
    p->next = s;
    
    • 语句顺序不唯一,但前两步必须在第四步之前
  4. 双链表的删除操作

    p->next = q->next;
    q->next->prior = p;
    free(q); // 释放结点空间
    

循环链表

  1. 循环单链表
    • 循环单链表的最后一个结点的指针不是 NULL,而是指向头结点,形成了一个环
    • 循环单链表判空的条件是:头结点是否等于头指针
    • 循环单链表在所有位置上的插入和删除操作都是等价的,不需要判断是否在表尾,因此,对循环单链表不设头指针而设尾指针,因为 r->next 就是头指针
  2. 循环双链表
    • 循环双链表中,对尾结点 *p 有:p->next = L
    • 循环双链表为空时,其头结点的 prior 域和 next 域都等于 L

静态链表

  1. 静态链表用数组来描述线性表的链式存储结构,它的结点也有数据域 data 和指针域 next,但指针是结点的相对地址(数组下标,又称游标)

  2. 与顺序表一样,静态链表也需要预先分配一块连续的内存空间

  3. 静态链表的结点类型描述如下:

    #define MaxSize 50 // 静态链表的最大长度
    typedef struct { // 定义静态链表结点类型
        int data; // 存储数据元素
        int next; // 下一个元素的数组下标
    } SLinkList[MaxSize];
    
  4. 静态链表以 next==-1 作为结束的标志

  5. 它的插入和删除操作与动态链表相同,只需要修改指针而不需要移动元素

  6. 在不支持指针的高级语言(如 Basic)中会方便一些