1
0
mirror of https://github.com/142vip/408CSFamily.git synced 2026-04-05 11:38:27 +08:00
Files
408CSFamily/docs/DataStructure/linear_table/3.chain_representation.md
mmdapl 42117a0dd9 init
2022-04-08 00:10:38 +08:00

13 KiB
Raw Blame History

线性表的链式表示

顺序表的插入、删除操作需要移动大量元素影响了运行效率虽然时间复杂度为O(1)的情况也存在)。

链式存储线性表时,不需要使用连续的存储单元,不要求逻辑上相邻的两个元素在物理位置上也相邻

理解“链”的含义,链条--->捆绑、指向------>指针

链式存储线性表时,对线性表的插入、删除元素是不需要移动元素的,只是需要修改指针

  • 单链表
  • 双链表
  • 循环链表
  • 静态链表

单链表

线性表的链式存储称作单链表,通过一组任意的存储单元来存储线性表中的数据元素。

每个链表结点node除了存放元素自身的信息外还需要存放一个指向其后继结点的指针。目的是通过指针建立起链表元素之间的线性关系

单链表中结点类型的描述:


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

单链表可以解决顺序表需要大量连续存储空间的缺点,但是单链表在数据域的基础上附加了指针域,存在浪费存储空间的缺点;

单链表的元素是离散地分布在存储空间中的,因此单链表是非随机存取的存储结构,不能直接找到表中特定的结点,需要从头开始遍历、一次查找;

通常,头指针用来标识一个单链表。头指针指向NULL时,标识单链表为空。

头结点

为了操作上的方便,在单链表第一个结点之前附加一个结点,叫做头结点

  • 头结点的数据域可以不存任何信息、也可以记录表长等基础信息
  • 头结点的指针域指向线性表的第一个元素结点;

不论单链表是否带头结点(可选),头指针始终指向链表的第一个结点。

头结点是带头结点的链表中的第一个结点【重要】

  • 头结点的数据域可以不存任何信息、也可以记录表长等基础信息
  • 头结点的指针域指向线性表的第一个元素结点;

头结点的优点:

  • 因为开始结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和在表的其他位置上的操作一致,不需要进行特殊处理;
  • 无论链表是否为空,头指针始终是指向头结点的头结点的非空指针【空表中,往往就只有头结点,此时头结点的指针域为空,可以有效避免头指针空指针异常问题】-----> 头结点的引入,很好的统一了空表和非空表的操作;

头插法

从空表开始,生成新的结点,将读取的数据存放在新结点的数据域中,将新结点插入到当前链表的表头【头结点之后】

/*
 * @Description: 单链表头插法创建
 * @Version: Beta1.0
 * @Author: 【B站&公众号】Rong姐姐好可爱
 * @Date: 2020-03-04 23:38:04
 * @LastEditors: 【B站&公众号】Rong姐姐好可爱
 * @LastEditTime: 2020-03-04 23:39:16
 */
LinkList CreateListWithStartNode(LinkList &L){
    
    LNode *s;
    int x;
    L=(LinkList)malloc(sizeof(LNode));  // 创建头结点L
    L->next=NULL;                       // 初始化空链表
    
    // 控制台输入值
    scanf("%d",&x);
    
    // 输入9999 表示结束
    while(x!==9999){
        // 开辟新结点存储空间
        s=(LNode*)malloc(sizeof(LNode));    
        // 结点数据域赋值
        s->data=x;      
        // 修改指针新结点插入表中【注意L->next为头结点的指针域】
        s->next=L->next;
        L->next=s;
        scanf("%d",&x);
    }
    
    // 返回单链表
    return L;
}


特点:

  • 读入数据的顺序与生成的链表中的元素顺序是相反的【结合队列先进先出思考】
  • 每个结点插入的时间复杂度为O(1),单链表长度为n时头插法的时间复杂度为O(n)【结合算法中的while循环可以很明确看出时间复杂度】

尾插法

头插法建立的单链表,链表中结点的次序和输入数据的顺序不一致【相反】,尾插法则很好的避免了这个问题;

新结点插入到当前链表的表尾上必须增加一个尾指针r,始终指向当前链表的尾结点;


/*
 * @Description: 单链表尾插法创建
 * @Version: Beta1.0
 * @Author: 【B站&公众号】Rong姐姐好可爱
 * @Date: 2020-03-04 23:38:04
 * @LastEditors: 【B站&公众号】Rong姐姐好可爱
 * @LastEditTime: 2020-03-04 23:39:16
 */
LinkList CreateListWithEndNode(LinkList &L){
    
    
    int x;              // 输入结点值
    L=(LinkList)malloc(sizeof(LNode));
    LNode *s;           // 新结点s
    LNode *r=L;         // r为尾指针
    
    // 控制台输入值
    scanf("%d",&x);
    
    while(x!==9999){
        // 开辟新结点存储空间
        s=(LNode *)malloc(sizeof(LNode));
        
        // 新结点s的数据域赋值为x
        s->data=x;
        // 单链表L的尾指针指向新的结点s
        r->next=s;
        
        // 指针r指向新的表尾结点
        r=s;

        scanf("%d",&x);
    }
    
    // 表尾指针置空【重要】
    r->next=NULL;

    // 返回单链表
    return L;
    
}

特点:

  • 读入数据的顺序与生成的链表中的元素顺序完全一致
  • 每个结点插入的时间复杂度为O(1),单链表长度为n时尾巴插法的时间复杂度为O(n)【结合算法中的while循环可以很明确看出时间复杂度】
  • 相比头插法附设了一个指向表尾结点的指针,但时间复杂度与头插法相同

按序号查找

在单链表中从第一个结点出发顺指针next域逐个往下搜索、遍历直到找出第i个结点为止否则返回最后一个结点指针域NULL


/*
 * @Description: 单链表按序号查找
 * @Version: Beta1.0
 * @Author: 【B站&公众号】Rong姐姐好可爱
 * @Date: 2020-03-04 23:38:04
 * @LastEditors: 【B站&公众号】Rong姐姐好可爱
 * @LastEditTime: 2020-03-04 23:39:16
 */
LNode *GetElem(LinkList L,int i){
    int j=1;                  // 查询计数初始为1
    LNode *p=L->next;         // 单链表头结点指针赋值给指针p
    

    // 第0个元素则指向头结点返回头结点
    if(i==0){
        // 头结点包含数据域和指针域
        return L;
    }
    
    // 不等于0却小于1则i为负数无效直接返回NULL查询结果空
    if(i<1){
        return NULL;
    }

    // p存在且计数没有走到初始i的位置
    while(p&&j<i){
        
        // 指针后移
        p=p->next;

        // 计数标记+1
        j++;
    }

    // 注意: 当p不存在时 跳出循环p=NULL; 当p存在但是j大于等于i跳出循环返回查找的结果返回p
    // 从跳出循环上来分析p要么存在即找到的结点元素要么为空即NULL

    // 跳出循环返回第i个结点的指针
    return p;
    
}

需要遍历扫描单链表时间复杂度为O(n)

按值查找

从单链表的第一个结点开始从前往后依次比较表中个结点数据域的值等于给定值e则返回该结点的指针若整个单链表【遍历完】中没有数据域值为e的结点则返回NULL


/*
 * @Description: 单链表按值查找
 * @Version: Beta1.0
 * @Author: 【B站&公众号】Rong姐姐好可爱
 * @Date: 2020-03-04 23:38:04
 * @LastEditors: 【B站&公众号】Rong姐姐好可爱
 * @LastEditTime: 2020-03-04 23:39:16
 */
LNode *LocateElem(LinkList L,ElemType e){
    
    // 指针【哨兵】
    LNode *p=L->next;
    // 从第1个结点开始查找数据域(data)为e的结点
    while(p!=NULL&&p->data!=e){
        // 无法匹配,指针后移
        p=p->next;
    }
    
    // 注意p为NULL的时候说明单链表已经遍历的尾结点了跳出循环没有找到目标结点

    // 查找到第1个匹配的结点跳出循环返回结点指针
    return p;
    // 
}

链表遍历无法匹配会返回NULL,因为在尾结点无法匹配的时候,直接返回尾结点指针域

需要遍历扫描单链表时间复杂度为O(n)

结点插入

单链表中将值为x的新结点插入到单链表的第i个位置上

  • 第一步: 检查插入位置的合法性;
  • 第二步: 找到待插入位置的前驱结点即第i-1个结点
  • 第三部: 在前驱结点后插入新结点;
    // 循环遍历时间复杂度O(n)
    p=GetElem(L,i-1);
    
    // 移动指针时间复杂度O(1)
    s->next=p->next;
    p->next=s;

结合上面的代码可以看出将元素x插入到单链表L的第i个元素上必须先找到单链表L的i个结点的前驱结点i-1的位置需要采用GetElem()函数,按照序号查找;

如果返回的前驱结点不为空则说明插入的位置i合法否则位置非法插入失败

找到前驱结点p后最重要的是移动指针将新的结点s的指针域指向结点p的指针域也就是s的指针域指向元素p的后继结点第i个结点元素

原来的(i-1)位置上的元素也就是前驱结点p的指针域则必须指向新的结点元素

上面的过程不能更换,避免后继指针不存在的问题

最后的最后一定要注意将s的数据域赋值x

插入结点的时间复杂度集中在查找第(i-1)个元素时间复杂度为O(n);如果在给定结点的后面插入新结点,只需要执行p->next=s操作时间复杂度为O(1)

前插操作

在某结点的前面插入一个新的结点

对结点的前插操作都可以转化为后插操作前提需要从单链表的头结点开始顺序查找到其前驱结点时间复杂度为O(n)。

后插操作

在某结点的后面插入一个新的结点,单链表插入算法中,通常采用后插操作的


// 结点s插入到结点p的前面修改指针域顺序不能改变
s->next=p->next;
p->next=s;


// 经典的借助变量,进行值交换
temp=p->data;
p->data=s->data;
s->data=temp;

上述借助临时变量temp来将结点s和结点p的数据域进行交换需要开辟O(1)的空间复杂度但是时间复杂度却从O(n)改变为O(1),典型的空间换时间策略

删除结点

将单链表L的第i个结点元素删除

  • 第一步: 先检查删除位置的合法性;
  • 第二步: 查找表中的第i-1个结点即被删结点的前驱结点
  • 第三步: 移动指针,删除结点元素;

// 获取删除位置结点元素的前驱结点
p=GetElem(L,i-1);

// 删除位置结点元素指针
q=p->next;

// 修改指针,将删除位置结点元素前驱结点的指针域指向其后继结点
p->next=q->next;

// 释放结点元素的内存控件
free(q)

和插入算法一样时间都消耗在查询前驱结点上时间复杂度为O(n)

删除单链表L中给点结点元素*p通常是按值查找获取到p结点的前驱元素再执行删除操作这样很明显会导致时间复杂度为O(n),主要都消耗在按值查找

这里可以利用p结点的后继结点将p结点删除

  • 第一步申请结点q使其只想p结点的后继结点
  • 第二步将p结点的数据域值换成其后继结点的数据域【注意交换没什么意义最终p的后继结点会删除、释放】
  • 第三步p的指针域指向q的指针域q结点从链中“断开”
  • 第四步释放q的内存空间
    // 存放p的后继结点指针
    q=p->next;
    
    // 结点p的后继结点元素赋值给结点p避免后继结点的数据域丢失
    p->data=p->next->data;
    p->next=q->next;
    
    // 此时q指向更换数据域后的p即原来p的后继结点
    free(q)

相比按值查找前驱结点来删除给定的结点p利用后继结点来删除的时间复杂度更小O(1)

计算表长

计算单链表中数据结点(不含头结点)的个数

算法思路:从第一个结点开始顺序依次访问表中的每一个结点,为此需要设置一个计数器变量每访问一个结点计算器加1直到访问到空结点为止。

算法时间复杂度O(n)

单链表的长度是不包括头结点的,不带头结点和带头结点的单链表在求表长操作上会略有不同。

不带头结点的单链表,当表为空时候,需要单独处理;

// 不带头结点的单链表L为空,判定条件是L=NULL。
if(L===NULL){
// 链表为空表长为0
    return 0;
}

// 带头结点的单链表L为空判空条件L->next=NULL;

if(L->next===NULL){
    // 链表为空不包含头结点表长为0
    return 0;
}