第二章 线性表考点

【考频统计】

年份考点分值
2009双指针问题(前后指针):查找单链表中倒数第 k 个结点15分
2010数组循环左移(数组逆置问题)13分
2011寻找两个数组的中位数15分
2012相交链表找交点15分
2013将两个升序链表合并为一条降序链表(选择)、数组主元素(摩尔投票法) 应用题线性表的顺序存储结构与链式存储结构的ASL问题25分
2014未考察
2015删除链表重复结点15分
2016单链表插入及其物理存储结构、双向循环链表删除结点操作、快排思想划分数组问题19分
2017未考察
2018数组中未出现的最小正整数13分
2019重排链表(快慢指针求中间结点、链表逆置、交叉合并链表)13分
2020三元组的最短距离(多指针)13分
2021单循环链表删除节点2分
2022未考察
2023顺序表的基本操作、非空双向链表删除结点4分

一、顺序表的基本操作

1.1 顺序表的定义

1.1.1 静态分配 – 定义

#define MAX_SIZE 1024 // 定义顺序表的最大容量
typedef int ElemType; // 定义元素类型为 int

typedef struct {
    ElemType data[MAX_SIZE]; // 静态数组,存储最多 MAX_SIZE 个元素
    int length; // 当前元素个数
} SqList;

1.1.2 动态分配 – 定义

typedef int ElemType; // 定义元素类型为 int

typedef struct {
    ElemType* array; // 动态分配的数组指针
    size_t max_size; // 数组最大容量
    size_t length; // 当前元素个数
} SeqList;

C语言中使用 malloc 和 free 进行空间的申请和释放 (C++则使用new和delete)。

1.1.3 静态分配 – 初始化

void InitList(SqList* L) {
    if (L == NULL) return; // 检查空指针
    L->length = 0; // 初始化长度为 0
}

1.1.4 动态分配 – 初始化

int InitList(SeqList* L, size_t max_size) {
    if (L == NULL || max_size == 0) return -1; // 检查空指针或无效容量
    L->array = (ElemType *)malloc(sizeof(ElemType) * max_size); // 分配数组内存
    if (L->array == NULL) return -1; // 检查内存分配失败
    L->max_size = max_size; // 设置最大容量
    L->length = 0; // 初始化长度为 0
    return 0; // 成功
}

1.2 插入操作

在顺序表的第i个位置(位序,非下标)插入元素x;

bool Insert(seqList &L, int i, int x) {
    if(i > L.length + 1 || i < 1 || L.length == MaxSize) {    //如果位序不在有效范围内或者顺序表已满,则插入失败;
        return false;  
    }
    for(int j = L.length; j >= i; --j) {    //将待插入元素后面的元素通通后移一位;
        L.data[j] = L.data[j-1];   
    }
    L.data[i-1] = x;   //在位序为i的位置放入元素x; 
    L.length ++;    //数组长度+1;
    return true;
}

插入操作时间复杂度计算

Pi 是在第 i 个位置插入一个节点的概率,则在长度n的顺序表中插入一个元素,所需移动元素的平均次数为

第二章 线性表考点

1.3 删除操作

删除顺序表L中的第i个元素;

bool ListDelete(seqList &L, int i ) {
    if(i < 1 || i > L.length)    //如果删除元素的位置不在有效范围内,则删除失败;
        return false;
    int a = L.data[i-1];    //用变量a接收删除的元素;
    for(int j = i - 1; j < L.length - 1; ++j) {    //将待删除元素的后面位置的元素通通前移一位;
        L.data[j] = L.data[j+1];
    }
    L.length --;   //长度减1;
    return true;
}

删除操作时间复杂度

第二章 线性表考点

1.4 按值查找

在顺序表中查找第一个值为x的元素,并返回其位序;

int LocateElem(seqList &L, int x) {
    for(int i = 0; i < L.length; ++i ) {
        if(L.data[i] == x)
            return i+1;
    }
    return -1;  //查找失败
}

按值查找的时间复杂度

第二章 线性表考点

由上面的几个操作可以看出,只要该操作涉及到遍历操作,那么其时间复杂度就为 O(n)

1.5 遍历输出

void ListVisit(seqList&L) {
    for(int i = 0; i < L.length; ++i) {
        printf("%d",L.data[i]);
    }
}

二、链表的定义和基本操作

2.1 单链表结点的数据结构描述

typedef struct LNode{
    int data;    //数据域
    struct LNode *next;    //代表指针域,指向直接后继元素;
}LNode,*LinkList;    //结构体别名

单链表是由表头指针唯一确定,因此单链表可以用头指针的名字来命名。若头指针名是L,则简称该链表为表L。

2.2 单链表的初始化

有头结点的单链表的初始化

LNode* InitList(void) {
    LNode* head = malloc(sizeof(LNode)); // 分配头节点内存
    if (head == NULL) return NULL; // 检查内存分配失败
    head->next = NULL; // 初始化为空链表
    head->data = 0; // 初始化数据字段(可选)
    return head; // 返回头节点指针
}
第二章 线性表考点

无头结点的单链表的初始化

//无头结点的单链表的初始化
LNode InitList(){
    LNode *head = NULL;    //直接指向NULL,初始为空链表
    return head;
}
第二章 线性表考点

由于带有头结点的链表更容易操作,因此如果题目中给的链表无头结点,可以自己申请一个 dummy 结点指向第一个结点,然后 dummy 就是新的头节点,之后就可以按照有头结点的链表进行操作

LNode* dummy = malloc(sizeof(LNode));
if (dummy == NULL) return NULL; // 检查内存分配失败
dummy->next = head; // 设置哑节点的下一个指针
dummy->data = 0; // 可选:初始化数据字段(视需求而定)

2.3 单链表求表长(不含头结点)

int ListLength(LinkList L) {
    if (L == NULL) return 0; // 检查无效链表

    LNode* p = L->next; // 从第一个数据节点开始(带头部节点的情况)
    int length = 0;

    while (p) { // 遍历链表,计数节点
        length++;
        p = p->next;
    }

    return length; // 返回链表长度
}

该操作遍历了整个链表,因此时间复杂度为O(n)

2.4 单链表按位查找

由于单链表不支持随机存取,所以只能从头结点开始遍历,直到找到目标结点,所以时间复杂度为 O(n)

LNode* getElem(LinkList L, int i) {
    if (L == NULL || i < 0) return NULL; // 检查无效链表或负索引

    LNode* p = L; // 从头节点开始(带头部节点的情况)
    
    // 遍历直到第 i 个节点或链表末尾
    while (p && i > 0) {
        p p->next;
        i--;
    }
    
    return p; // 返回第 i 个节点指针,或未找到返回 NULL
}

2.5 单链表的按值查找

LNode* LocateElem(LinkList L, int target) {
    if (L == NULL) return NULL; // 检查输入链表是否为空
    
    LNode* p = L->next; // 从第一个数据节点开始(带头部节点的情况)
    
    while (p && p->data != target) { // 遍历直到找到目标或链表末尾
        p = p->next;
    }
    
    return p; // 返回指向目标节点的指针,或未找到返回 NULL
}

2.6 头插法建立单链表

LinkList List_head_insert(LNode *&L) {    //若链表要改变的话注意带上&
    LNode *newNode;
    int x;      //假设输入x = -1 代表输入结束
    L = (LinkList)malloc(sizeof(LNode));   // 创立头结点;
    L->next = NULL;   //初始化为空链表;
    scanf("%d", &x);    //输入结点的值;
    while(x != -1) {
        newNode = (LNode*)malloc(sizeof(LNode));
        newNode->data = x;
        newNode->next = L->next;
        L->next = newNode;
        scanf("%d", &x);    // 读取下一个值
    }
    return L;
}

采用头插法建立链表时,读入数据的顺序与生成链表中的元素的顺序是相反的,该重要特性常应用与链表的逆置。

由于插入每个结点的时间复杂度为O(1),设单链表表长为 n,则总的时间复杂度为 O(n).

2.7 尾插法建立单链表

LinkList List_tail_insert(LNode *&L) {        //LNode *&L == LinkList &L
    LNode *newNode, *tail;     //tail为指向链表的表尾指针;
    int x;
    L = (LinkList) malloc(sizeof(LNode));
    L->next = NULL;
    tail = L;      // 初始时尾指针指向头结点
    scanf("%d", &x);
    while(x != -1) {
        newNode = (LNode*)malloc(sizeof(LNode));
        newNode->data = x;
        newNode->next = NULL;
        tail->next = newNode;
        tail = newNode;     // 尾指针指向新插入的节点
        scanf("%d", &x);    // 读取下一个值
    }
    return L;
}

尾插法关键点在于建立一个始终指向表尾的指针Lnode *r,因此插入的总体时间复杂度为O(n)

2.8 插入结点操作

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

后插法,找到第 i-1 个元素将新结点插入其后面

/**
 * 在单链表的第i个位置插入新节点
 * @param L 指向头指针的引用,用于修改头指针
 * @param i 插入位置,从1开始计数
 * @param x 新节点的数据值
 * @return 插入成功返回true,失败返回false
 */
bool InsertNode(LNode *&L, int i, int x) {
    // 定义三个指针:新节点、前驱节点、当前节点
    LNode *newNode, *preNode, *curNode;
    
    // 插入位置合法性检查,i必须大于0
    if (i <= 0) return false;
    
    // 查找第i-1个节点,即新节点的前驱节点
    preNode = getElem(L, i-1);
    
    // 如果前驱节点不存在(i超出链表长度),插入失败
    if (preNode == NULL) return false;
    
    // 保存前驱节点的下一个节点,即原来的第i个节点
    curNode = preNode->next;
    
    // 创建新节点并分配内存
    newNode = (LNode*)malloc(sizeof(LNode));
    
    // 检查内存分配是否成功
    if (newNode == NULL) {
        return false; // 内存分配失败
    }
    
    // 设置新节点的数据域和指针域
    newNode->data = x;
    newNode->next = curNode;
    
    // 将前驱节点的next指向新节点,完成插入
    preNode->next = newNode;
    
    return true; // 插入成功
}

该算法由于在查找第i-1个位置时涉及到了遍历操作,因此时间复杂度为 O(n);

扩展:由上面的操作可知,对某个结点的前插操作可以转化为后插操作,前提是要找到第i-1个结点,如果题目已经给出了当前结点,我们可以将待插入结点插入当前结点的后面,然后交换这两个结点的值,那么就可以实现 O(1) 时间复杂度的算法

例如:将值为x的结点插入P结点的后面

void  insertnode(LNode *p,int x ){
    LNode *newNode;
    newNode = (LNode*)malloc(sizeof(LNode));
    newNode -> data = p-> data;      //交换数据域
    newNode -> next = p -> next;    //修改指针域,注意不能颠倒顺序;
    p -> next = newNode;
    p -> data = x;    //交换数据域
}

2.9 删除结点操作

  • 如果仅仅给出了要删除结点的位置 i,那么需要调用getElem来找到第i-1个结点p,然后p->next = p -> next -> next,再将该结点释放掉即可,由于查找i-1个结点需要遍历,因此该程序的时间复杂度为O(n);
  • 如果给出了当前结点的指针,那么要删除该结点的话就可以用下面的方法:既然不能干掉自己,那就变成儿子,在干掉儿子
/**
 * 删除指定节点的后续节点(优化版)
 * @param p 指向要删除节点的前一个节点的指针
 * @return 删除成功返回true,失败(p为空或p的下一个节点为空)返回false
 */
bool deleteNextNode(LNode *p) {
    // 防御性编程:检查输入指针是否有效
    if (p == NULL || p->next == NULL) {
        return false; // 删除失败
    }
    
    LNode *q = p->next;         // 保存待删除节点的指针
    p->next = q->next;          // 从链表中移除待删除节点
    free(q);                    // 释放待删除节点的内存
    return true;                // 删除成功
}

本网站原创文章版权归何大锤的狂飙日记所有。发布者:何大锤,转转请注明出处:何大锤的博客

(0)
何大锤的头像何大锤管理团队

相关推荐

  • 第二章 线性表 代码实现

    心得汇总 一、线性表的定义和基本操作 二、线性表的顺序表示 三、线性表的链式表示 3.1 链表结点定义 这个C程序演示了一个简单的链表实现,包含两个节点。让我来分析一下它的功能: 代码分析 结构体定义: 主函数逻辑: 预期输出结构 程序会输出类似这样的内容: 关键要点 这是一个展示C语言中链表节点创建、连接和遍历的基础示例。 优化后的代码

    2025年7月5日
    200
  • 第二章 线性表 手写笔记

    一、线性表的定义和基本操作 二、线性表的顺序表示 2.1 顺序表的定义 2.2 顺序表的初始化 2.3 插入操作 2.4 删除操作 2.5 按值查找 三、线性表的链式表示 3.1 链表的定义 3.2 链表结点创建 逻辑结构图1 逻辑结构图2

    2025年7月5日
    200
  • 第二章 习题集

    一、重要知识点 1、仅有尾指针的单循环链表 优于仅有头指针的单循环链表,有尾指针相当于也提供了头指针 2、在单链表某结点前插入结点,只知道当前节点是无法实现的 3、只知道当前节点,也可以删除该节点,把后面的元素的值复制到当前,例如3.14 4、如果是链表,线性表的第 i 个元素时间与 i 的大小有关;如果是顺序表则是随机存储。 5、静态链表与动态链表相比的缺…

    2025年7月4日
    000

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们

2211932694

在线咨询: QQ交谈

邮件:hdcblog1999@163.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信
网站建设中ing......