第二章 线性表

线性表

线性表的定义

  • 线性表示具有相同数据类型的 n(n >= 0)个数据元素有限序列,其中 n 为表长,当 n = 0时线性表是一个空表。若用L命名线性表,则其一般表示为 L = (a1, a2, ai, ai+1, an)。每个数据元素所占空间一样大。
    • 有次序。
    • 有限(所有整数按递增次序排列不是一个线性表)。
    • ai 是线性表中的“第 i 个”元素线性表的位序
    • a1表头元素;an表尾元素
    • 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继

线性表的基本操作

  • InitList(&L)初始化表。构造一个空的线性表 L,分配内存空间
  • Destroy(&L)销毁操作。销毁线性表,并释放线性表 L 所占用的内存空间
  • LinstInsert(&L, i, e)插入操作。在表L中的第 i 个位置上插入指定元素e。
  • ListDelete(&L, i, &e):删除操作。删除表 L 中的第 i 个位置的元素,并用 e 返回删除元素的值。
  • LocateElem(L, i)按值查找操作。在表 L 中查找具有给定关键字值的元素。
  • GetElem(L, i)按位查找操作。获取表 L 中第 i 个位置的元素的值。
  • Length(L):求表长。返回线性表 L 的长度,即 L 中数据元素的个数。
  • PrintList(L):输出操作。按前后顺序输出线性表 L 的所有元素值。
  • Empty(L):判空操作。若 L 为空表,则返回 true,否则返回 false。

顺序表

顺序表的定义

  • 用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

顺序表的实现

  • 静态分配

    #define MaxSize 10
    typedef struct {
        ElemType data[MaxSize];
        int length;
    }SqList;
    
  • 动态分配

    #define InitSize 10
    typedef struct {
        ElemType* data;
        int MaxSize;
        int lengeh;
    }SeqList;
    
    //增加动态数组的长度
    void IncreaseSize(SeqList& L, int len) {
        int* p = L.data;
        L.data = (int*)malloc((L.MaxSize + len) * sizeof(int));
        for(int i = 0; i < L.length; i++) {
            L.data[i] = p[i];
    	}
        L.MaxSize = L.MaxSize + len;
        free(p);
    }
    
  • 顺序表的特点

    • 随机访问,即可以在 O(1) 时间内找到第i个元素。
    • 存储密度高,每个结点只存储数据元素。
    • 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)。
    • 插入、删除操作不方便,需要移动大量元素。

顺序表的插入和删除

  • 插入操作。在表 L 中的第 i 个位置上插入指定元素e。

    bool LinstInsert(SqList& L, int i, int e) {
        if(i < 1 || i > L.length + 1)
            return false;
        if(L.length >= MaxSize)
            return false;
        for(int j = L.length; j >= i; j--) {
            L.data[j] = L.data[j - 1];
        }
        L.data[i - 1] = e;
        L.length++;
        return true;
    }
    

    最好时间复杂度:O(1)

    最坏时间复杂度:O(n)

    平均时间复杂度:O(n)

  • 删除操作。删除表 L 第 i 个位置的元素,并用 e 返回删除元素的值。

    bool ListDelete(SqList& L, int i, int& e) {
        if(i < 1 || i > L.length)
            return false;
        e = L.data[i - 1];
        for(int j = i; j < L.length; j++) {
            L.data[j - 1] = L.data[j];
            L.length--;
            return true;
        }
    }
    

    最好时间复杂度:O(1)

    最坏时间复杂度:O(n)

    平均时间复杂度:O(n)

顺序表的查找

  • 按位查找操作。获取表 L 中第 i 个位置的元素的值。

    ElemType GetElem(SqList L, int i) {
        return L.data[i - 1];
    }
    

    时间复杂度:O(1)

  • 按值查找操作。在表 L 中查找具有给定关键字值的元素。

    int LocateElem(SeqList L, ElemType e) {
        for(int i = 0; i < L.length; i++)
            if(L.data[i] == e)
                return i + 1;
        return 0;
    }
    

    最好时间复杂度:O(1)

    最坏时间复杂度:O(n)

    平均时间复杂度:O(n)

单链表

单链表的定义

  • 顺序表:每个结点中只存放数据元素。

    • 优点:可随机存取,存储密度高。
    • 缺点:要求大片连续空间,改变容量不方便。
  • 单链表:每个结点除了存放数据元素外,还要存储指向下一个结点的指针。

    • 优点:不要求大片连续空间,改变容量方便。
    • 缺点:不可随机存取,要耗费一定空间存放指针。
  • 结构体定义

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

    强调这是一个单链表:使用LinkList

    强调这是一个结点:使用LNode*

  • 不带头结点的单链表

    bool InitList(LinkList& L) {
        L = NULL;// 空表,暂时还没有任何结点(防止脏数据)
        return true;
    }
    
  • 带头结点的单链表

    bool InitList(LinkList& L) {
        L = (LNode*)malloc(sizeof(LNode));
        if(L == NULL)
            return false;
        L->next = NULL;
        return true;
    }
    

    头结点不存储数据。

单链表的插入

  • 插入操作。在表L中的第i个位置上插入指定元素e。

    • 带头结点
    bool ListInsert(LinkList& L, int i, ElemType e) {
        if(i < 1)
            return false;
        LNode* p;
        int j = 0;
        p = L;
        while(p != NULL && j < i-1) {
            p = p->next;
            j++;
    	}
        if(p == NULL)
            return false;
        LNode* s = (LNode*)malloc(sizeof(LNode));
        s->data = e;
        s->next = p->next;//此句和下一句不能颠倒
        p->next = s;
        return true;
    }
    

    最好时间复杂度:O(1)

    最坏时间复杂度:O(n)

    平均时间复杂度:O(n)

    • 不带头结点
    bool ListInsert(LinkList& L, int i, ElemType e) {
        if(i < 1)
            return false;
        if(i == 1) {
            LNode* s = (LNode*)malloc(sizeof(LNode));
            s->data = e;
            s->next = L;
            L = s;
            return true;
        }
        LNode* p;
        int j = 1;
        p = L;
        while(p != NULL && j < i-1) {
            p = p->next;
            j++;
    	}
        if(p == NULL)
            return false;
        LNode* s = (LNode*)malloc(sizeof(LNode));
        s->data = e;
        s->next = p->next;
        p->next = s;
        return true;
    }
    
  • 后插操作。在 p 结点之后插入元素。

    bool InsertNextNode(LNode* p, ElemType e) {
        if(p == NULL)
            return false;
        LNode* s = (LNode*)malloc(sizeof(LNode));
        if(s == NULL)// 某些情况下内存分配失败(如内存不足)
            return false;
        s->data = e;
        s->next = p->next;
        p->next = s;
        return true;
    }
    
  • 前插操作。在 p 结点之前插入元素 e。

    bool InsertPriorNode(LNode* p, ElemType e) {
        if(p == NULL)
            return false;
        LNode* s = (LNode*)malloc(sizeof(LNode));
        if(s == NULL)
            return false;
        s->next = p->next;
        p->next = s;//新节点s连到p之后
        s->data = p->data;//将p中元素复制到s中
        p->data = e;//p中元素覆盖为e
        return true;
    }
    

    时间复杂度:O(1)

单链表的删除

  • 删除操作。删除表中第 i 个位置的元素,并用 e 返回删除元素的值。

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

    最好时间复杂度:O(1)

    最坏时间复杂度:O(n)

    平均时间复杂度:O(n)

  • 删除指定结点p

    bool DeleteNode(LNode* p) {
        if(p == NULL)
            return false;
        LNode* q = p->next;
        p->data = p->next->data;
        p->next = q->next;
        free(q);
        return true;
    }
    

    注意:不满足p是最后一个结点的情况。若需要删除最后一个结点,只能从表头依次寻找p的前驱。

单链表的查找

  • 按位查找,返回第i个元素(带头结点)

    LNode* GetElem(LinkList L, int i) {
        if(i < 0)
            return NULL;
        LNode* p;
        int j = 0;
        p = L;
        while(p != NULL && j < i) {
            p = p->next;
            j++;
    	}
        return p;
    }
    

    平均时间复杂度:O(n)

  • 封装(基本操作)的好处:避免重复代码,简洁、易维护。

  • 按值查找,找到数据域 == e的结点。

    LNode* LocateElem(LinkList L, ElemType e) {
        LNode* p = L->next;
        while(p != NULL && p->data != e)
            p = p->next;
        return p;
    }
    
    
  • 求表的长度。

    int Length(LinkList L) {
        int len = 0;
        while(p->next != NULL) {
            p = p->next;
            len++;
    	}
    }
    

    时间复杂度:O(1)

单链表的建立

  • 尾插法建立单链表

    LinkList List_TailInsert(LinkList& L) {
        int x;
        L = (LinkList)malloc(sizeof(LNode));
        LNode* s,* r = L;
        scanf("%d", &x);
        while(x != 9999) {//输入9999表示结束
            s = (LNode*)malloc(sizeof(LNode));
            s->data = x;
            r->next = s;
            r = s;
            scanf("%d", &x);
        }
        r->next = NULL;
        return L;
    }
    

    注意:设置一个指向表尾结点的指针。

  • 头插法建立单链表

    LinkList List_HeadInsert(LinkList& L) {
        LNode *s;
        int x;
        L = (LinkList)malloc(sizeof(LNode));
        L->next = NULL;
        scanf("%d", &x);
        while(x != 9999) {//输入9999表示结束
            s = (LNode *)malloc(sizeof(LNode));
            s->data = x;
            s->next = L->next;
            L->next = s;
            scanf("%d", &x);
        }
        return L;
    }
    
    

双链表

  • 单链表:无法逆向检索,有时候不太方便。

  • 双链表:可双向检索,存储密度略低于单链表。

  • typedef struct DNode {
        ElemType data;
        struct DNode* prior, * next;
    }DNode, * DLinkList;
    
  • 双链表的初始化(带头结点)

    bool InitDLinkList(DLinklist& L) {
        L = (DNode*)malloc(sizeof(DNode));
        if(L == NULL)//内存不足,分配失败
            return false;
        L->prior = NULL;
        L->next = NULL;
        return true;
    }
    
  • 双链表的插入

    bool InsertNextDNode(DNode* p, DNode* s) {
        if(p == NULL || s == NULL)
            return false;
        s->next = p->next;//将s结点插入到p结点之后
        if(p->next != NULL)
        	p->next->prior = s;
        s->prior = p;
        p->next = s;
    }
    
  • 双链表的删除

    bool DeleteNextDNode(DNode* p) {//删除p结点的后继结点q
        if(p == NULL)
            return false;
        DNode* q = p->next;
        if(q == NULL)
            return false;
        p->next = q->next;
        if(q->next != NULL)
            q->next->prior = p;
        free(q);
        return true;
    }
    
  • 双链表的遍历

    • 后向遍历
    while(p != NULL) {
        //对结点p做相应处理
        p = p->next;
    }
    
    • 前向遍历
    while(p != NULL) {
        //对结点p做相应处理
        p = p->prior;
    }
    
    • 前向遍历(跳过头结点
    while(p->prior != NULL) {
        //对结点p做相应处理
        p = p->prior;
    }
    
  • 双链表不可随机存取,按位查找、按值查找操作都只能用遍历方法实现。时间复杂度O(n)

循环链表

  • 单链表:从一个结点出发只能找到后续的各个结点。

  • 循环单链表:从一个结点出发可以找到其他任何一个结点。

  • 初始化循环单链表

    bool InitList(LinkList& L) {
        L = (LNode*)malloc(sizeof(LNode));
        if(L == NULL)//内存不足,分配失败
            return false;
        L->next = L;//头结点next指向头结点
        return true;
    }
    
  • 在循环单链表中如果需要经常对表头/表尾结点操作,可将表头/表尾设为头结点。

  • 初始化循环双链表

    bool InitDLinkList(DLinklist& L) {
        L = (DNode*)malloc(sizeof(DNode));
        if(L == NULL)
            return false;
        L->prior = L;
        L->next = L;
        return true;
    }
    
  • 循环双链表的插入

    bool InsertNextDNode(DNode* p, DNode* s) {
        s->next = p->next;//将s结点插入到p结点之后
        p->next->prior = s;
        s->prior = p;
        p->next = s;
    }
    
  • 循环双链表的删除

    bool DeleteNextDNode(DNode* p) {//删除p结点的后继结点q
        DNode* q = p->next;
        p->next = q->next;
        q->next->prior = p;
        free(q);
        return true;
    }
    

静态链表

  • 定义:分配一整片连续的内存空间,各个结点集中安置(用数组方式实现的链表)。

    • 与数组不同之处:数组各个元素是按照数据下标0至n依次排序的。而在每个结点包含数据元素和下一个结点的数组下标(游标)两个信息,因而静态链表不一定依次排序。
    • 每个数据元素4B,每个游标4B(每个结点8B),设起始地址为addr(不是e0地址),则e1的存放地址为addr + 8*2。
  • 结构体定义

    #define MaxSize 10
    struct Node {
        ElemType data;
        int next;//下一个数据下标
    }
    
    #define MaxSize 10
    typedef struct {
        ElemType data;
        int next;
    }SLinkList[MaxSize]
    
    • SLinkList a可用 SLinkList 定义“一个长度为 MaxSize 的 Node 型数组”
  • 初始化静态链表:

    • 把 a[0] 的 next 设为-1。
    • 把其他结点的 next 设为一个特殊的值来表示空闲,如-2。
  • 查找:从头结点出发依次往后遍历结点。

  • 插入位序为i的结点:

    1. 找到一个空的结点,存入数据元素。
    2. 从头结点出发找到位序为 i-1 的结点。
    3. 修改新结点的 next。
    4. 修改 i-1 号结点的 next。
  • 删除某个结点:

    1. 从头结点出发找到前驱结点。
    2. 修改前驱结点的游标。
    3. 被删除结点的 next 设为-2。
  • 静态链表的优点:增删操作不需要大量移动元素

  • 静态链表的缺点:

    • 不能随机存取,只能从头结点开始依次往后查找。
    • 容量固定不可变。

顺序表和链表的比较

逻辑结构

  • 都属于线性表,都是线性结构。

存储结构

  • 顺序表:
    • 优点:支持随机存取、存储密度高。
    • 缺点:大片连续空间分配不方便,改变容量不方便。
  • 链表:
    • 优点:离散的小空间分配方便,改变容量方便。
    • 缺点:不可随机存取,存储密度低。

基本操作

  • 初始化:
    • 顺序表:需要预分配大片连续空间。若分配内存过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。
      • 静态分配:静态数组:容量不可改变。
      • 动态分配:动态数组(malloc、free):容量可以改变,但需要移动大量元素,时间代价高。
    • 链表:只需分配一个头结点(也可以不要头结点,只声明一个头指针),以后方便拓展。
  • 销毁:
    • 顺序表:修改length=0。
      • 静态分配(静态数组):系统自动回收空间。
      • 动态分配(动态数组:malloc、free):需要手动 free。
    • 链表:依次删除各个结点(free)。
  • 插入删除:
    • 顺序表:需要将后续元素后移/前移。
      • 时间复杂度:O(n)。时间开销主要来自于移动元素。若数据元素很大,则移动的时间代价很高。
    • 链表:只需要修改指针。
      • 时间复杂度:O(n)。时间开销主要来自于查找元素。查找元素的时间代价更低。
  • 查找:
    • 顺序表:
      • 按位查找:O(1)
      • 按值查找:O(n)。若表内元素有序,则可在O(log2n)时间内找到。
    • 链表:
      • 按位查找:O(n)
      • 按值查找:O(n)
  • 表长难以预估、经常要增加/删除元素:链表。
  • 表长可预估、查询(搜索)操作较多:顺序表。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值