数据结构(严蔚敏版)——第二章《线性表》

127 阅读22分钟

这是我参与「掘金日新计划 · 2 月更文挑战」的第 二十四 天

第二章 线性表

2.0、回顾

数据结构

2.1、线性表的定义和特点

线性表的定义

线性表是n(n >= 0)个数据元素(结点) a1, a2, … ai-1, ai,ai+1, … an 组成的有限序列 。

n = 0时称为空表

线性表的特点:

对于非空的线性表或线性结构:

  1. 存在唯一的一个被称作"第一个"的数据元素
  2. 存在唯一的一个被称作"最后一个"的数据元素
  3. 除第一个之外,结构中的每一个数据元素均只有一个前驱
  4. 除最后一个之外,结构中的每一个数据元素均只有一个后继

2.2、线性表的类型定义

抽象数据类型线性表的定义:

ADT List{
  数据对象: D = {ai | ai in ElemtSet, i = 1, 2, ... ,n, n >= 0 };
  数据关系: R = {<ai-1, ai> | ai-1, ai in D, i = 2, ... ,n};
  基本操作: 
    InitList(&L);
        操作结果: 构造一个空的线性表;
    DestroyList(&L);
        初始条件: 线性表L已存在;
        操作结果: 销毁线性表L;
    ClearList(&L);
        初始条件: 线性表L已存在;
        操作结果: 清空线性表L;
    ListEmpty(&L);
        初始条件: 线性表L已存在;
        操作结果: 若L为空表,则返回true, 否则返回false;
    ListLength(&L);
        初始条件: 线性表L已存在;
        操作结果: 返回L中数据元素的个数;
    GetElem(L, i, e);
        初始条件: 线性表L已存在, 且1 <= i <= ListLength(L);
        操作结果: 用e返回L中的第i个数据元素的值;
    LocateElem(L, e);
        初始条件: 线性表L已存在,
        操作结果: 返回L中的第1个值和e相等的元素在L中的位置。若这样的元素不存在,则返回值为0;
    PriorElem(L, cur_e, &pre_e);
        初始条件: 线性表L已存在,
        操作结果: 若cur_e是L的数据元素,且不是第一个,则用pre_e返回其前驱,否则操作失败,pre_e无定义;
    NextElem(L, cur_e, &next_e);
        初始条件: 线性表L已存在,
        操作结果: 若cur_e是L的数据元素,且不是第一个,则用next_e返回其后继,否则操作失败,next_e无定义;
    ListInsert(&L, i, e);
        初始条件: 线性表L已存在, 且1 <= i <= ListLength(L) + 1;
        操作结果: 在L中第i个位置之前插入新的数据元素e,L的长度+1;
    ListDelete(&L, i);
        初始条件: 线性表L已存在且非空, 且1 <= i <= ListLength(L);
        操作结果: 删除L中第i个数据元素,L的长度-1;
    TraverseList(L);
        初始条件: 线性表L已存在;
        操作结果: 对线性表L进行遍历,在遍历过程中对L的每个结点访问一次。
}ADT List

2.3、线性表的顺序表示和实现

2.3.1、线性表的顺序存储和表示

顺序存储的定义: 把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。

顺序表中元素存储位置的计算:

线性表的每个元素需占用 l 个存储单元

  • 线性表中的第i+1个数据元素和第i个数据元素的存储位置为:LOC(a_i+_1) = LOC(a_i) + l
  • 线性表的第i个数据元素ai的存储位置为:LOC(a_i) = LOC(a_1) + (i - 1) \times l

顺序表(元素):

  • 地址连续
  • 依次存放
  • 随机存取
  • 类型相同

⚫️顺序表的存储结构:

#define MAX 100;                                    // 线性表存储空间的初始分配量
typedef struct {
  ElemType *elem;                                   // 存储空间的基地址
  int length;                                           // 当前长度
}SqList;                                                  // 顺序表的结构类型为SqList                                    

⚫️多项式的顺序存储结构类型:

#define MAX 100                     // 多项式可能达到的最大长度typedef struct {                    // 多项式非零项的定义
  float p;                              // 系数
  int e;                                    // 指数
}Pal;
​
typedef struct{
  Pal *elem;                            // 存储空间的基地址
  int length;                           // 多项式中当前项的个数
}SqList;                                    // 多项式的顺序存储结构类型为SqList

⚫️图书表的顺序存储结构:

#define MAXSIZE 10000               // 图书表可能达到的最大长度typedef struct {                            // 图书信息定义
  char no[20];                              // 图书ISBN
  char name[50];                            // 图书名字
  float price;                              // 图书价格
}Book;
​
typedef struct {
  Book *elem;                                   // 存储空间的基地址
  int length;                                   // 图书表中当前图书个数
}SqList;                                            // 图书表的顺序存储结构类型为SqList

补充一:C语言的内存动态分配 需加载头文件<stdlib.h>

SqList L;
L.data = (ElemType*)malloc(sizeof(ElemType)*MaxSize);

🏴malloc(m)函数:开辟m字节长度的地址空间,并返回这段空间的首地址

🏴sizeof(x)运算:计算变量x的长度

🏴free(p)函数:释放指针p所指变量的存储空间,即彻底删除一个变量

补充二:C++中的参数传递

◼️函数调用时传送给形参表的实参必须与形参三个一致:类型、个数、顺序

◼️参数传递有两种方式

  • 传值方式(参数为整型、实型、字符型等)

  • 传地址

    • 参数为指针变量
    #include<iostream>
    void swap(float *m, float *n) {
      float t;
      t = *m;
      *m = *n;
      *n = t;
    }
    void main() {
      float a, b, *p1, *p2;
      cin >> a >> b;
      p1 = &a;
      p2 = &b;
      swap(p1, p2);
      cout << a << endl << b << endl;
    }
    
    • 参数为引用类型
    #include<iostream>
    void swap(float& m, float& n) {
      float temp;
      temp = m;
      m = n;
      n = temp;
    }
    void main() {
      float a, b;
      cin >> a >> b;
      swap(a, b);
      cout << a << endl << b << endl;
    }
    
    • 参数为数组名

2.3.2、线性表基本操作的实现

补充:操作算法中用到的预定义常量和类型:

// 函数结果状态代码#define TRUE 1#define FALSE 0#define OK 1#define ERROR 0#define INFEASIBLE -1#define OVERFLOW -2// Status是函数的类型,其值是函数结果状态代码typedef int Status;
​
typedef char ElemType;

1、线性表的L的初始化(参数用引用)

  • 为顺序表L动态分配一个预定义大小的数组空间,使elem指向这段空间的基地址
  • 将表的当前长度设为0
Status InitList_Sq(SqList &L) {                 // 构造一个空的顺序表
  L.elem = new ElemType[MAXSIZE];               // 为顺序表分配空间大小为MAXSIZE的数组
  if(!L.elem) exit(OVERFLOW);                       // 存储分配失败
  L.length = 0;                                                 // 空表长度为0
  return OK;
}

2、销毁线性表

  • 使用delete 释放存储空间即可
void DestroyList(SqList &L) {
  if (L.elem) delete L.elem;            // 释放存储空间
}

3、清空线性表L

  • 将线性表的长度设置为0
void ClearList(SqList &L) {
  L.length = 0;                         // 将线性表的长度设置为0
}

补充:销毁与清空线性表的最大区别在于线性表是否还暂用存储空间

4、求线性表的长度

int GetLength(SqList L) {
  return (L.length);
}

5、判断线性表是否为空

int IsEmpty(SqList L) {
  if (L.length == 0) return 1;
  else return 0;
}

6、顺序表的取值(根据指定的位置序号i,获取第i个数据元素的值)

  • 判断i值是否合理(1 <= i <= L.length),若不合理返回ERROR
  • 若i值合理,则将第i个数据元素L.elem[i - 1]赋给参数e,通过e返回第i个数据元素的传值。
int GetElem(SqList L, int i, ElemType &e) {
  if (i < 1 || i > L.length) return ERROR;      // 判断i值是否合理,若不合理返回ERROR
  e = L.elem[i - 1];                                                    // 第i - 1的单元存储着第i个数据
  return OK;
}

7、顺序表的查找

  • 从第一个元素起,依次和e比较相等的元素L.elem[i],则查找成功,返回该元素的序号i + 1;
  • 若查遍整个顺序表都没有找到,则查找失败,返回0
int LocateElem(SqList L, ElemType e) {          // 在顺序表中查找值为e的数据元素,返回其序号
  for (int i = 0; i < L.length; i++)                
    if (L.elem[i] == e) return i + 1;               // 查找成功,返回序号i + 1
  return 0;                                                                 // 查找失败,返回0
}
​
// while循环
int LocateElem(SqList L, ElemType e) {                          // 在线性表L中查找值为e的数据元素,返回其序号
  i = 0;
  while (i < L.length && L.elem[i] != e) i++;
  if (i < L.length) return i + 1;                                       // 若查找成功,返回序号
  return 0;                                                                                 // 查找失败,返回0
}

8、顺序表的插入

  • 判断插入位置i是否合法(i值的合法范围是1 <= i <= n + 1),若不合法则返回ERROR。
  • 判断顺序表的存储空间是否已满,若满则返回ERROR
  • 将第n个至第i - 1个位置的元素依次向后移动一个位置,空出第i个位置(i = n + 1时无需移动)
  • 将要插入的新元素e放入第i个位置
  • 表长加1
Status ListInsert(SqList &L, int i, ElemType e) {
  // 在顺序表L中的第i个位置插入新元素e,i值的合法范围是1 <= i <= L.length + 1
  if ((i < 1) || (i > L.length)) return ERROR;      // i值不合法
  if (L.length == MAXSIZE) return ERROR;                    // 当前存储空间已满
  for (j = L.length - 1; j >= i - 1; j--)               
    L.elem[j + 1] = L.elem[j];                                      // 插入位置之后的元素后移
  L.elem[i - 1] = e;                                                            // 将新元素e放入第i个位置
  ++L.length;                                                                           // 表长加1
  return OK;
} 

9、顺序表的删除

  • 判断删除位置i是否合法(和法值为1 <= i <= n),若不合法则返回ERROR
  • 将第i + 1个至第n个的元素依次向前移动一个位置(i = n时无须移动)
  • 表长减1
Status ListDelete(SqList &L, int i) {
  // 在顺序表中删除第i个元素,i值的合法范围是1 <= i <= L.length
  if ((i < 1) || (i > L.length)) return ERROR;              // i值不合法
  for (j = i; j <= L.length - 1; j++)
    L.elem[j - 1] = L.elem[j];                                              // 被删除元素之后的元素前移
  --L.length;                                                                                   // 表长减1
  return OK;
}

小结:顺序表(线性表的顺序存储结构)的特点

  1. 利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致

  2. 在访问线性表时,可以快速的计算出任何一个数据元素的存储地址。

  3. 基本操作:重点:查找、插入、删除操作

  4. 算法分析:

    • 时间复杂度:插入、查找、删除算法的平均时间复杂度为O(n)
    • 空间复杂度:顺序表操作算法的空间复杂度S(n) = O(1)
  5. 顺序表的缺点:

    • 在插入、删除某一个元素时,需要移动大量元素
    • 浪费存储空间
    • 静态存储形式,数据元素的个数不能自由扩充

顺序表的代码实现(C语言、Java)

C语言:

#include<stdio.h>
#include<stdlib.h>#define MAXSIZE 100     // 定义顺序表的最大长度
​
typedef struct
{
    int data[MAXSIZE];
    int length;
}SqList;
​
/**初始化顺序表*/
void InitList(SqList *L)
{
    L ->length = 0;
}
​
/**建立顺序表*/
int CreateList(SqList *L, int a[], int n)
{
    if (n > MAXSIZE)
    {
        printf("空间不足,无法建立顺序表。\n");
        return 0;
    }
    for (int i = 0; i < n; i++)
    {
        L->data[i] = a[i];
    }
    L->length = n;
    return 1;
}
​
/**判断顺序表是否为空*/
int IsEmpty(SqList *L)
{
    if (L->length == 0)
        return 1;
    else return 0;
}
​
/**清空顺序表*/
void ClearList(SqList *L)
{
    L->length = 0;
}
​
/**求线性表的长度*/
int GetLength(SqList *L)
{
    return (L->length);
}
​
/**打印顺序表*/
void PrintfList(SqList *L)
{
    for (int i = 0; i < L->length; i++)
        printf("%d ", L->data[i]);
}
​
/**顺序表的取值*/
int GetElem(SqList *L, int i, int e)
{
    if (i < 1 || i > L->length) return 0;
    e = L->data[i - 1];
    return 1;
}
​
/**按值查找*/
int LocateElem(SqList *L, int x)
{
    for (int i = 0; i < L->length; i++)
        if (L->data[i] == x) return i + 1;
    return 1;
}
​
/**按位置查找*/
int GetLocate(SqList *L, int x, int *prt)
{
    if (x < 1 || x > L->length)
    {
        printf("查找位置非法,查找错误。\n");
        return 0;
    }
    else
    {
        *prt = L->data[x];
        return 1;
    }
}
​
​
/**插入操作*/
int ListInsert(SqList *L, int i, int x)
{
    if (L->length > MAXSIZE)
    {
        printf("上溢错误。\n");
        return 0;
    }
    if (i < 1 || i > L->length)
    {
        printf("插入位置错误!\n");
        return 0;
    }
    for (int k = L->length; k > i; k--)
    {
        L->data[k] = L->data[k - 1];
    }
    L->data[i] = x;
    L->length++;
    return 1;
}
​
/**顺序表的删除*/
int DeleteElem(SqList *L, int i, int *prt)
{
    if ((i < 1) || (i > L->length))
    {
        printf("删除位置错误!\n");
        return 0;
    }
    if (L->length == 0)
    {
        printf("顺序表为空删除失败!");
        return 0;
    }
    *prt = L->data[i - 1];
    for (int k = i; k <= L->length - 1; k++)
    {
        L->data[k - 1] = L->data[k];
    }
    L->length--;
    return 1;
}
​
​
int main()
{
    int a[5] = {1, 2, 3, 4, 5};
    int i, x;
    SqList list;
    InitList(&list);
    if (IsEmpty(&list))
    {
        printf("初始化顺序表成功!\n");
    }
    printf("给顺序表赋值:1 2 3 4 5\n");
    CreateList(&list, a, 5);
    PrintfList(&list);
    printf("\n在第三位后插入一个20:\n");
    ListInsert(&list, 3, 20);
    PrintfList(&list);
    if (DeleteElem(&list, 4, &x) == 1)
    {
        printf("\n把第四位删除,删除值是%d\n", x);
        PrintfList(&list);
    }
    return 0;
}

Java语言:

package struct;
​
import java.util.Arrays;
​
/**
 * @author java小豪
 * @version 1.0.0
 * @description 顺序表的实现
 */
public class SqList {
  
    /**存储数据的空间*/
    private int[] data;
    /**有效数据的长度*/
    private int length;
​
    public SqList() {
        this.data = new int[10];
    }
​
    /**
     * 顺序表的插入
     * @param pos 插入位置
     * @param x 插入的数据
     */
    public void add(int pos, int x) {
        // 判断顺序表是否为满
        if (this.data.length == length) {
            System.out.println("顺序表满了,扩充");
            this.data = Arrays.copyOf(this.data, this.data.length * 2);
        }
        // 判断插入位置是否合法
        if (pos < 0 || pos > this.length) {
            System.out.println("插入位置不合法");
        }
        // 插入元素
        for (int i = this.length - 1; i >= pos; i--) {
            this.data[i + 1] = this.data[i];
        }
        this.data[pos] = x;
        length++;
    }
​
    /**
     * 查找元素
     * @param x 查找的元素
     * @return 返回该元素的位置
     */
    public int search(int x) {
        // 判断顺序表是否为空
        if (this.length == 0 ) {
            throw new RuntimeException("顺序表为空");
        }
        // 查找该元素的位置
        for (int i = 0; i < this.data.length; i++) {
            if (this.data[i] == x) {
                return i;
            }
        }
        return -1;
    }
​
​
    public void delete(int x) {
        // 判断是否含有该元素
        int index = search(x);
        if (index == -1) {
            System.out.println("删除的元素不存在");
        }
        // 删除元素
        for (int i = index; i < this.length; i++) {
            this.data[i] = this.data[i + 1];
        }
        length--;
    }
​
    /**
     * 修改顺序表中的元素
     * @param pos 修改元素的位置
     * @param x 修改后的值
     */
    public void modify(int pos, int x) {
        // 判断顺序表是否为空
        if (this.length == 0) {
            throw new RuntimeException("顺序表为空");
        }
        // 判断pos位置有效性
        if (pos < 0 || pos > this.length) {
            throw new RuntimeException("删除位置不和法");
        }
        // 修改元素
        this.data[pos] = x;
    }
​
    /**
     * 打印顺序表
     */
    public void print() {
        for (int i = 0; i < this.length; i++) {
            System.out.print(this.data[i] + " ");
        }
        System.out.println();
    }
​
    public static void main(String[] args) {
        SqList sqList = new SqList();
        sqList.add(0, 1);
        sqList.add(0, 2);
        sqList.add(0, 3);
        sqList.add(0, 4);
        sqList.add(0, 5);
        System.out.println("=====================查找====================");
        sqList.print();
        System.out.println("查找元素下标为:" + sqList.search(1));
        System.out.println("查找元素下标为:" + sqList.search(3));
        System.out.println("查找元素下标为:" + sqList.search(5));
        System.out.println("=====================删除====================");
        sqList.delete(1);
        sqList.delete(5);
        sqList.print();
        System.out.println("=====================修改====================");
        sqList.modify(0,10);
        sqList.modify(1,20);
        sqList.print();
​
​
    }
}

2.4、线性表的链式存储表示与实现

结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻

线性表的链式表示又称为非顺序映像或链式映像

链式存储结构特点:

  • 用一组物理位置任意的存储单元来存放线性表的数据元素
  • 这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的
  • 链表中元素的逻辑次序和物理次序不一定相同

结点的组成:数据域、指针域

2.4.1、与链式存储有关的术语

  1. 结点: 数据元素的存储映像。由数据域和指针域两部分组成
  2. 链表: n个结点由指针链组成一个链表。
  3. 单链表: 结点只有一个指针域的链表,称为单链表或线性表
  4. 双链表: 结点由两个指针域的链表,称为双链表
  5. 循环链表: 首尾相接的链表称为循环链表
  6. 头指针: 是指向链表中第一个结点的指针
  7. 首元结点: 是指链表中存储第一个数据元素a_1的结点
  8. 头结点: 是在链表的首元结点之前附设的一个结点

单链表的分类:

  • 带头节点
  • 不带头节点

2.4.2、单链表的表示

1、存储结构

typedef struct Lnode {          // 声明结点的类型和指针结点的指针类型
  ElemType data;                        // 结点的数据域
  struct Lnode *next;               // 结点的指针域
}Lnode, *LinkList;                  // LinkList为指向结构体Lnode的指针类型

定义链表L: LinkList L;

*定义结点指针P: LNode P 等同于LinkList p;

存储学生学号、姓名成绩的单链表表示:

typedef struct {
  char num[8];              // 数据域
  char num[8];              // 数据域
  int score                 // 数据域
} ElemType;
​
typedef struct Lnode {
  ElemType data;                        // 数据域
  struct Lnode *next;               // 指针域
} Lnode, *LinkList;

2.4.3、单链表基本操作的实现

1、单链表的初始化(带头结点的单链表)

  • 生成新结点作为头结点,用头指针L指向头结点
  • 头结点的指针域为空
Status InitList(LinkList &L)
{ // 构造一个空的单链表
  L = new LNode;                        // 生成新结点作为头结点,用头指针L指向头结点
  // L = (LinkList) malloc (sizeof(LNode));
  L -> next = NULL;                 // 头结点的指针域为空
  return OK;
}

2、判断链表是否为空

空表:链表中五元素,称为空链表(头指针和头结点仍然在)

  • 判断指针域是否为空
int ListEmpty(LinkList L)   // 若L为空表,则返回1,否则返回0
{
  if (L -> next )                   // 非空
    return 0;
  else 
    return 1;
}

3、单链表的销毁(销毁后链表不存在)

  • 从头指针开始,依次释放所有结点

image-20220924233512857

Status DestroyList(LinkList &L)             // 销毁单链表L
{
  Lnode *p;                                                         // 或LinkList p;
  while (L) {
    p = L;
    L = L -> next;
    delete p;
  }
  return OK;
}

3、清空单链表

链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍然在)

  • 依次释放所有结点,并将头结点指针与设置为空

image-20220924234759060

Status ClearList(LinkList &L) {     // 将L重置为空表
  Lnode *p, *q;                                     // 或LinkList p,q;
  P = L -> next;
  while (p) {                                           // 没到表尾
    q = p -> next;
    delete p;
    p = q;
  }
  L -> next = NULL;                             // 头结点指针域 
  return OK;
  
}

4、求单链表的表长

  • 从首元结点开始,依次计数所有结点

image-20220925094016564

int ListLength(LinkList L) {        // 返回L中数据元素个数
  LinkList *p;
  p = L -> next;                                    // p指向第一个结点
  int i = 0;
  while (p) {
    i++;                                                    // 遍历单链表,统计结点数
    p = p -> next;
  }
  return i;
}

image-20220925211651549

5、单链表的取值

  • 用指针p指向首元结点,用j做计数器初值赋为1

  • 从首元结点开始依次开始顺着链域next向下访问,只要指向当前结点的指针P不为空(NULL),并且没有达到序号为i结点,则循环执行以下操作:

    • p指向下一个结点
    • 计数器j相应加1
  • 退出循环时,如果指针p为空,或者计数器j 大于i,说明指定的序号i值不和法(i大于表长n或i小于等于0)取值失败返回ERROR;否则取值成功,此时j = i时,p所指的结点就是要找到的第i个结点,用参数e保存当前结点的数据域,返回OK。

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

6、单链表的按值查找

  • 用指针p指向首元结点
  • 从首元结点开始依次顺着链域next向下查找,只要指向当前结点的指针p不为空,并且p所指结点的数据域不等于给定值e,则循环执行以下操作:p指向下一个结点
  • 返回p。若查找成功,p此时即为结点的地址值,若查找失败,p的值即为NULL

根据指定数据获取该元素数据的地址:

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

根据指定数据获取该数据位置序号:

int LocateElem(LinkList L, ElemType e) 
{//返回L中值为e的数据元素的位置序号,查找失败返回0
  p = L -> next; j = 1;
  while (p && p -> data != e) {
    p = p -> next ;
    j++;
  }
  if (p) return j;
  else return 0;
}

7、单链表的插入

  • 将值为e的新结点插入到表的第i个结点的位置上,即插入到结点ai-1与ai之间
  • 查找结点ai-1并由指针p指向该结点。
  • 生成一个新结点*s
  • 将新结点*s的数据域置为e,s -> data = e;
  • 将新结点*s的指针域指向结点ai, s -> next = p -> next;
  • 将结点p的指针域指向新结点s,p -> next = s;
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) return ERROR;                // i > n+1 或者 i < 1
  s = new LNode;                                                        // 生成新结点*s
  s -> data = e;                                                        // 将结点*s的数据域置为e
  s -> next = p -> next;                                        // 将结点*s的指针域指向结点ai
  p -> next = s;                                                        // 将结点*p的指针域指向结点*s
  return OK;
}

8、单链表的删除

【算法步骤】:

  • 删除单链表的第i个结点ai
  • 查找结点ai-1并由指针p指向该结点
  • 临时保存待删除结点ai的地址在q中,以备释放
  • 将结点*p的指针域指向ai的直接后继结点
  • 释放结点ai的空间
Status ListDelete(LinkList &L, int i)
{// 在带头结点的单链表L中,删除第i个元素
  p = L; j = 0;
  while ((p -> next) && (j < i - 1)) {
    p = p > next;                                               // 查找第i - 1个结点,p指向该结点
    ++j;
  }
  if (!(p -> next) || (j > i - 1)) return ERROR;        // 当i > n 或 i < 1时删除位置不合理
  q = p -> next;                                                                        // 临时保存被删除结点的地址以备释放
  p -> next = p -> next -> next;                                        // 改变删除结点前驱结点的指针域
  delete q;                                                                                 // 释放删除结点空间
  return OK;
}

9、单链表的建立(头插法)

  • 创建一个只有头结点的空链表

  • 根据带创建链表包括的元素个数n,循环n次执行以下操作:

    • 生成一个新结点*p;
    • 输入元素赋值给新结点*p的数据域
    • 将新结点*p插入到头结点之后

image-20220926081750621

void CreateList_H(LinkList &L, int n)
{// 逆序位输入n个元素值,建立带头结点的单链表L
  L = new LNode;
  L -> next = NULL;                                     // 先建立一个带头结点的空链表
  for (i = n; i > 0; --i)
  {
    p = new LNode;                                      // 生成新结点*p
    cin >> p -> data;                                   // 输入元素赋给新结点*p的数据域
    p -> next = L -> next;
    L -> next = p;                                      // 将新结点*p插入到头结点之后
  }
}

4、单链表的建立(尾插法)

  • 创建一个只有头结点的空链表

  • 尾指针r初始化,指向头结点

  • 根据创建链表包括的元素个数n,循环n次执行以下操作:

    • 生成一个新结点*p
    • 输入元素值赋给新结点*p的数据域
    • 将新结点p插入到尾结点r之后
    • 尾指针r指向新的尾结点*p
void CretaeList_R(LinkList &L, int n)
{// 正位序输入n个元素值,建立带头结点的单链表L
  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; r ->next = p;                     // 将新结点*p插入尾结点*r之后
    r = p;                                                                      // r指向新的尾结点
  }
}

2.4.4、循环链表

循环链表: 是一种头尾相接的链表(即:表中最后一个结点的指针域指向头结点,整个链表形成一个环)

注意:

  • 由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件不再是p或p->next是否为空而是他们是否等于头指针

  • 循环条件

    image-20220926154555415

  • 头指针表示单循环链表

    • 找a1的时间复杂度:O(1)
    • 找an的时间复杂度:O(n)

注意:表的操作常常是在表的首位位置上进行

  • 尾指针表示单循环链表

    • a1的存储位置是:R -> next -> next
    • an的存储位置是:R
  • 尾指针循环链表的合并(将Tb合并在Ta之后)

p =Ta -> next;
Ta -> next = Tb -> next -> next;
delete Tb -> next;
Tb -> next = p;

image-20220926160814163

【算法描述】:

LinkList Connect(LinkList Ta, LinkList Tb)
{// 假设Ta、Tb都是非空的单循环链表
  p = Ta -> next;                                                   // p存表头结点
  Ta -> next = Tb -> next -> next;              // Tb表头连结Ta表尾
  delete Tb -> next;                                            // 释放Tb表头结点
  Tb -> next = p;                                                   // 修改指针
  return Tb;
}

2.4.5、双向链表

1、双向链表的结构定义如下:

typedef struct DuLNode {
  ElemType data;                                    // 数据域
  struct DuLNode *prior;                    // 前驱指针
  struct DuLNode *next;                     // 后继指针
}DuLNode, *DuLinkList;

2、双向链表的插入

image-20220926164041286

Status ListInsert_DuL(DuLinkList &L, int i, ElemType e)
{// 在带头结点的双向循环链表L中第i个位置之前插入元素e
  if (!(p = GetElemP_DuL(L,i))) return ERROR;
  s = new DuLNode;
  s -> data = e;
  s -> prior = p -> prior;
  p -> prior -> next = s;
  s -> next = p;
  p -> prior = s;
  return OK;
}

3、双向链表的删除

image-20220926165219182

算法描述:

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

单链表、循环链表和双向链表的时间效率比较

查找表头结点 (首元节点)查找表尾结点查找结点*p的前驱结点
带头结点的单链表LL ->next 时间复杂度O(1)从L -> next 依次向后遍历 时间复杂度O(n)通过p -> next无法找到其前驱
带头结点仅设尾指针L的循环单链表L ->next 时间复杂度O(1)从L -> next 依次向后遍历 时间复杂度O(n)通过p -> next 可以找到其前驱时间复杂度O(n)
带头结点仅设尾指针R的循环单链表R ->next 时间复杂度O(1)R 时间复杂度O(1)通过p -> next 可以找到其前驱时间复杂度O(n)
带头结点的双向循环链表LL ->next 时间复杂度O(1)L -> prior 时间复杂度O(1)p -> prior 时间复杂度O(1)

2.5、顺序表和链表的比较

链式存储结构的优点

  • 结点空间可以动态申请和释放
  • 数据元素的逻辑次序靠结点的指针来指示,即插入和删除时不需要移动数据元素。

链式存储结构的缺点

  • 存储密度小,每个结点的指针域需要额外占用存储空间
  • 链式存储结构是非随机存储结构。对任一结点的操作都要从头指针依指针链查找到该结点
  • 存储密度 = 结点数据本身占用的空间\div 结点占用的空间总量

顺序表域链表的比较

2.6、线性表的应用

2.6.1、线性表的合并

问题描述:

  • 已知两个集合A和B,现要求一个新的集合A=A∪B。例如,设A = (7, 5, 3, 11)和B = (2, 6, 3)合并后A = (7, 5, 3, 11, 2, 6)

算法步骤:

  • 分别获取La表长m和Lb表长n

  • 从Lb中第一个元素开始,循环n次执行以下操作:

    • 从Lb中查找第i(1 <= i <= n)个数据元素赋给e
    • 在La中查找元素e,如果不存在,则将e插在表La的最后

线性表的合并

void MergeList(List &La, List Lb) {
  // 将所有在线性表Lb但不在La中的数据元素插入到La中
  m = ListLength(La); n = ListLength(Lb);               // 求线性表的长度
  for (i = 0; i <= n; i++) {
    GetElem(Lb, i, e);                                                  // 取Lb中第i个数据元素赋给e
    if (!LocateElem(La, e));                                        // La中不存在和e相同的数据元素
      ListInsert(La, ++m, e);                                       // 将e插在La的最后
  }
}

C语言实现:

2.6.2、顺序有序表的合并

问题描述:

  • 已知线性表La和Lb中的元素按值非递减有序排列,现要求将La和Lb归并为一个新的线性表Lc,且Lc中的数据元素仍按值非递减有序排列La = (1, 7, 8)和Lb = (2, 4, 6, 8, 10, 11)合并后为Lc = (1, 2, 4, 6, 7, 8, 8, 10, 11)

算法步骤:

  • 创建一个表长为m + n的空表LC
  • 指针pc初始化,指向LC的第一个元素
  • 指针pa和pb初始化,分别指向LA和LB的第一个元素
  • 当指针pa和pb均未到达相应表尾时,则依次比较pa和pb所指向的元素值,从LA或LB中摘取元素值较小的结点插入到LC的最后
  • 如果pb已达到LB的表尾,依次将LA的剩余元素插入LC的最后
  • 如果pa已到达LA的表尾,依次将LB的剩余元素插入LC的最后

算法描述:

void MergeList_Sq(SqList LA, SqList LB, SqList LC)
{ // 已知顺序表LA和LB的元素按值非递减排列
  // 归并LA和LB得到新的顺序有序表LC,LC的元素也按值非递减排列
  LC.length = LA.length + LB.length;                                // 先表长为待合并两表的长度之和
  LC.elem = new ELemType[LC.length];                                // 为合并后的新表分配一个数组空间
  pc = LC.elem;                                                                         // 指针pc指向新表的第一个元素
  pa = LA.elem;              pb = LB.elem;                              // 指针pa和pb的初值分别指向两个表的第一个元素
  pa_last = LA.elem + LA.length - 1;                                // 指针pa_last指向LA的最后一个元素
  pb_last = LB.elem + LB.length - 1;                                // 指针pb_last指向LB的最后一个元素
  while ((pa <= pa_last) &&(pb <= pb_last))                 // LA和LB均未到达表尾
  {
    if (*pa <= *pb) *pc++ = *pa++;                                  // 依次摘取两个表中值较小的结点插入到LC的最后
    else *pc++ = *pb++;
  }
  while (pa <= pa_last) *pc++ = *pa++;                          // LB已到达表尾,依次将LA的剩余元素插入到LC的最后
  while (pb <= pb_last) *pc++ = *pb++;                          // LA已到达表尾,依次将LB的剩余元素插入到LC的最后
}

复杂度分析:

  • 时间复杂度:O(ListLength(La) + ListLength(Lb))
  • 空间复杂度:O(ListLength(La) + ListLength(Lb))

2.6.3、链表有序表的合并

问题描述:

  • 已知线性表La和Lb中的元素按值非递减有序排列,现要求将La和Lb归并为一个新的线性表Lc,且Lc中的数据元素仍按值非递减有序排列La = (1, 7, 8)和Lb = (2, 4, 6, 8, 10, 11)合并后为Lc = (1, 2, 4, 6, 7, 8, 8, 10, 11)

算法步骤:

  • 指针pa和pb初始化,分别指向LA和LB的第一个结点
  • LC的结点取值为LA的头结点
  • 指针pc初始化,指向LC的头结点
  • 当指针pa和pb均未到达相应表尾时,则依次比较pa和pb所指的元素值,从LA或LB中摘取元素值比较小的结点插入到LC的最后
  • 将非空列表的剩余段插入到pc所指结点之后
  • 释放LB的头结点

算法描述:

void MergeList_L(LinkList &LA, LinkList &LB, LinkLIst &LC)
{ // 已知单链表LA和LB的元素按值非递减排列
  // 归并LA和LB得到新的单链表LC,LC的元素也按值非递减排列
  pa = LA->next;pb = LB->next;                                      // pa和pb的初值分别指向两个表的第一个结点
  LC = LA;                                                                              // 用LA的头结点作为LC的头结点
  pc = LC;                                                                              // pc的初值指向LC的头结点
  while(pa && pb)
  { // LA和LB均为到达表尾,依次摘取两表中较小的结点插入到LC的最后
    if (pa->data <= pb->data)                                           // 摘取pa所指结点
    {
      pc->next = pa;                                                            // 将pa所指结点链接到pc所指结点之后
      pc = pa;                                                                      // pc指向pa
      pa = pa->next;                                                            // pa指向下一个结点
    }
    else                                                                                    // 摘取pb所指结点
    {
      pc->next = pb;                                                            // 将pb所指结点链接到pc所指结点之后
      pc = pb;                                                                      // pc指向pb
      pb = pb->next;                                                            // pb指向下一个结点
    }
  }                                                                                             // while
  pc->next = pa ? pa : pb;                                              // 将非空表的剩余段插入到pc所指结点之后
  delete LB;                                                                            // 释放LB的头结点
}

复杂度分析:

  • 时间复杂度:O(ListLength(La) + ListLength(Lb))
  • 空间复杂度:O(ListLength(La) + ListLength(Lb))

2.7、案例分析与实现

1、一元多项式的运算:实现两个多项式加、减乘运算

设计内容:

用顺序存储结构实现一元多项式的加法、减法和乘法。具体要求为:用五个函数分别实现一元多项式的创建、输出、加法、减法和乘法;

设计思路:

将顺序表数组下标作为多项式的指数项,数组内的数据元素存放多项式的系数,通过访问数组内元素的同时获取下标并对二者进行不同的运算后,将运算结果依旧按原形式放入新的数组中,完成对两个多项式的加减乘运算。

测试数据:

多项式1:10 + 5x -4x^2 + 3x^3 + 2x^4(即输入10 0 5 1 -4 2 3 3 2 4)(输入-1 -1结束)

多项式2:-3 + 8x + 4x^2 - 5x^4 + 7x^5 -2x^6(即输入 -3 0 8 1 4 2 -5 4 7 5 -2 6)(输入 -1 -1结束)

代码实现:

#include <stdio.h>
#include <stdlib.h>
​
/**表可能达到的最大长度,存储空间初始分配量*/
#define MAXSIZE 20/**表的数据类型,根据实际情况而定*/
typedef int ElemType;
​
typedef struct {
    /**数组存储数据元素,最大值为MAXSIZE*/
    ElemType *data;
    /**最高次项*/
    int highPower;
} SqList;
​
/**操作结果:构造一个空的线性表L*/
void InitList(SqList *L) {
    /**申请连续的MAXSIZE长度空间*/
    L->data = (ElemType *) malloc(sizeof(ElemType) * MAXSIZE);
    /**判断空间是否申请成功*/
    if (!L->data)
        exit(-1);
    for (int i = 0; i < MAXSIZE; i++)
        L->data[i] = 0;
    /**空表最高次项为0*/
    L->highPower = 0;
}
​
/**销毁线性表L*/
void DestroyList(SqList *L) {
    /**释放data指向的空间*/
    free(L->data);
    L->data = NULL;
    L->highPower = 0;
}
​
/**创建多项式:由用户输入多项式的每项系数与指数*/
void CreatePolynomial(SqList *L) {
    int coefficient, exponent;
​
    /**循环输入常数项的每一项*/
    for (int i = 0; i < 10; i++)
    {
        printf("\n请输入第%d项的常数项和指数项,结束请输入-1 -1:", i + 1);
        scanf("%d%d", &coefficient, &exponent);
        /**指数为-1则结束输入*/
        if (exponent != -1) 
        {
            /**数组下标为指数项,常数项存入下标对应的位置,若有相同指数项则常数项相加*/
            L->data[exponent] += coefficient;
            /**指数项最大项为多项式长度*/
            if (L->highPower < exponent) 
                L->highPower = exponent;
        } else
            break;
    }
    printf("输入完毕\n");
}
​
/**输出常数项:按多项式指数大小依次输出多项式每一项*/
void PrintPolynomial(SqList L) {
    printf("多项式为:");
​
    /**若常数项为0则不输出,若不为0则只输出常数项*/
    if (L.data[0] != 0)
        printf("%d", L.data[0]);
​
    /**若常数项为0则不输出*/
    if (L.data[1] > 0)
        printf("+%dx", L.data[1]);/**不输出指数项为1*/
    else if (L.data[1] < 0)
        printf("%dx", L.data[1]);
​
    /**从第二项开始输出指数项*/
    for (int i = 2; i <= L.highPower; i++)
        /**若常数项为0则不输出该项*/
        if (L.data[i] > 0)
            printf("+%dx^%d", L.data[i], i);
        else if (L.data[i] < 0)
            printf("%dx^%d", L.data[i], i);
​
    printf("\n");
}
​
/**两多项式相加*/
void TwoPolynomialAdd(SqList L1, SqList L2, SqList *L3) {
    /**获取两多项式的最大项以确定相加后的多项式的最大项*/
    L3->highPower = L1.highPower > L2.highPower ? L1.highPower : L2.highPower;
    /**通过循环将两个多项式的同指数项的系数相加并保存到新的多项式中*/
    for (int i = 0; i <= L3->highPower; i++)
        L3->data[i] = L1.data[i] + L2.data[i];
}
​
/**两多项式相减*/
void TwoPolynomialSub(SqList L1, SqList L2, SqList *L3) {
    /**获取两多项式的最大项以确定相加后的多项式的最大项*/
    L3->highPower = L1.highPower > L2.highPower ? L1.highPower : L2.highPower;
    /**通过循环将两个多项式的同指数项的系数相减并保存到新的多项式中*/
    for (int i = 0; i <= L3->highPower; i++)
        L3->data[i] = L1.data[i] - L2.data[i];
}
​
/**两多项式相乘*/
void TwoPolynomialMul(SqList L1, SqList L2, SqList *L3) {
    /**两多项式的最高项相加为新多项式的最高项*/
    L3->highPower = L1.highPower + L2.highPower;
    for (int i = 0; i <= L1.highPower; i++)
        /**通过双层循环将两个多项式的每一项两两相乘与并保存到新的多项式中*/
        for (int j = 0; j <= L2.highPower; j++)
            /**相乘后的指数项为两项指数项相加*/
            L3->data[i + j] += L1.data[i] * L2.data[j];
}
​
int Menu() {
    int a;
    printf("输入1创建两个多项式\t\t输入2输出两个多项式\n");
    printf("输入3将两个多项式相加 \t\t输入4将两个多项式相减\n");
    printf("输入5将两个多项式相乘 \t\t输入0退出\n");
    printf("请输入:");
    scanf("%d", &a);
    return a;
}
​
int main() {
    SqList L1, L2, L3;
    InitList(&L1);
    InitList(&L2);
    InitList(&L3);
​
    while (1) {
        switch (Menu()) {
            case 1:
                printf("\n请输入多项式 1 :");
                CreatePolynomial(&L1);
                printf("\n多项式 1 为:\n");
                PrintPolynomial(L1);
                printf("\n请输入多项式 2 :");
                CreatePolynomial(&L2);
                printf("\n多项式 2 为:\n");
                PrintPolynomial(L2);
                break;
            case 2:
                printf("\n多项式 1 为:\n");
                PrintPolynomial(L1);
                printf("\n多项式 2 为:\n");
                PrintPolynomial(L2);
                break;
            case 3:
                InitList(&L3);
                TwoPolynomialAdd(L1, L2, &L3);
                printf("两多项式相加后的结果");
                PrintPolynomial(L3);
                break;
            case 4:
                TwoPolynomialSub(L1, L2, &L3);
                printf("两多项式相减后的结果");
                PrintPolynomial(L3);
                break;
            case 5:
                DestroyList(&L3);
                InitList(&L3);
                TwoPolynomialMul(L1, L2, &L3);
                printf("两多项式相乘后的结果");
                PrintPolynomial(L3);
                break;
            case 0:
                exit(0);
            default:
                printf("暂无此功能\n");
        }
    }
}

运行结果:

image-20221001100254659

image-20221001100418985

相加后的多项式为:7+13x+3x^3-3x^4+7x^5-2x^6

相减后的多项式为:13-3x-8x^2+3x^3+7x^4-7x^5+2x^6

相乘后的多项式为:-30+65x+92x^2-21x^3-48x^4+73x^5+43x^6-53x^7+19x^8+8x^9-4x^10

2、稀疏多项式的运算

案例实现

用链表表示多项式时,每个链表结点存储多项式中的一个非零项,包括系数(coef)和指数(expn)两个数据域以及一个指针域(next)。

typedef struct PNode {
  float coef;                           // 系数
  int expn;                             // 指数
  struct PNode *next;           // 指针域
} PNode, *Polunomial;

多项式的创建

算法步骤
  • 创建一个只有头结点的空链表

  • 根据多项式的项的个数n,循环n次执行以下操作:

    • 生成一个新结点*s
    • 输入多项式当前项的系数和指数赋给新结点*s的数据域
    • 设置一前驱指针pre,用于指向待找到的第一个大于输入项指数的结点的前驱,pre初值指向头结点
    • 指针q初始化,指向元首结点
    • 循链向下逐个比较链表中当前结点与输入项指数,找到第一个大于输入项指数的结点*q
    • 将输入项结点s插入到结点q之前
算法描述
void CreatePolyn(Polynomial &P, int n) {                
  P = new PNode;                                                                // 输入m项的系数和指数,建立表示多项式的有序链表P
  P -> next = NULL;                                                         // 先建立一个头结点的单链表
  for (i = 1; i <= n; i++) {                                        // 依次输入n个非零项
    s = new PNode;                                                          // 生成新结点
    cin >> s -> coef >> s -> expn;                          // 输入系数和指数
    pre = P;                                                                        // pre用于保存q的前驱,初值为头结点
    q = P -> next;                                                          // q初始化,指向首元结点
    while (q && q -> expn < s -> expn) {                // 找到第一个大于输入项数的项*q
      pre = q;
      q = q -> next;
    }
    s -> next = q;                                                        // 将输入项s插入到q和其前驱结点pre之间
    pre -> next = s;
  }
}

多项式的相加

算法步骤
  • 指针p1和p2初始化,分别指向Pa和Pb的首元结点

  • p3指向和多项式的当前结点,初值为Pa的头结点

  • 当指针p1和p2均为达到相应表尾时,则循环比较p1和p2所指结点对应的指数值(p1->expn 与 p2 -> expn),有下列3种情况:

    • 当p1->expn 等于 p2 -> expn时,则将两个结点种的系数相加,若和不为零,则修改p1所指结点的系数值,同时删除p2所指结点,若和为零,则删除p1和p2所指结点
    • 当p1->expn 小于p2 -> expn时,则应摘取p1所指结点插入到”和多项式“链表中去
    • 当p1->expn 大于p2 -> expn时,则应摘取p2所指结点插入到”和多项式“链表中去
  • 将非空多项式的剩余段插入到p3所指结点之后

  • 释放Pb的头结点

算法描述
void AddPolyn(Polynomial &Pa, Polynmial &Pb) {
  // 多项式加法:Pa = Pa + Pb,利用两个多项式的结点构成”和多项式“
  p1 = Pa -> next; p2 = Pb -> next;
  p3 = Pa;
  while (p1 && p2) {
    if (p1 -> expn == p2 -> expn) {
      sum = p1 -> coef + p2 -> coef;
      if (sum != 0) {
        p1 -> coef = sum ;
        p3 -> next = p1; p3 = p1;
        p1 = p1 -> next;
        r = p2; p2 = p2 -> next; delete r;
      } else {
        r = p1; p1 = p1 -> next; delete r;
        r = p2; p2 = p2 -> next; delete r;
      }
    } else if (p1 -> expn < p2 -> expn) {
      p3 -> next = p1;
      p3 = p1;
      p1 = p1 -> next;
    } else {
      p3 -> next = p2;
      p3 = p2;
      p2 = p2 -> next;
    }
  }
  p3 -> next = p1 ? p1 : p2;
  delete Pb;
}