数据结构基础详解(C语言):单链表_定义_初始化_插入_删除_查找_建立操作_纯c语言代码注释讲解

简介: 本文详细介绍了单链表的理论知识,涵盖单链表的定义、优点与缺点,并通过示例代码讲解了单链表的初始化、插入、删除、查找等核心操作。文中还具体分析了按位序插入、指定节点前后插入、按位序删除及按值查找等算法实现,并提供了尾插法和头插法建立单链表的方法,帮助读者深入理解单链表的基本原理与应用技巧。

单链表理论知识详解

1.单链表的定义

线性表的链式存储.
优点:不要求大片连续空间,改变容量方便
缺点:不可随机存取,要耗费一定空间存放指针

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

typedef 取别名
将struct LNode 取别名为别的,方便书写
比如我们要声明一个该结构体的时候
由原先的struct LNode a; 可以直接写为LNode a;
由原先的struct LNode *p; 可以直接写为LinkList a;

2.单链表的初始化

带头结点的初始化,头结点就是多一个结点,指向第一个存放数据的结点.
不带头结点,会使处理数据的逻辑更复杂,对==空表和非空表需要不同的代码逻辑==.
单链表的初始化本质:为头结点分配一个堆空间,将头结点指针域置为空,加上判断内存是否能分配

#include <stdio.h>
#include <stdlib.h>
//这是带有头结点的单链表初始化
void InitList() {
   
    LinkList L;//定义头指针变量 
    L=(LNode*)malloc(sizeof(LNode));//头指针指向分配的头结点内存空间 
    L->next=NULL;
    return true;
}
int main()
{
   
    InitList( );
}

3.单链表的插入和删除

3.1 单链表的插入

3.1.1 按位序插入

按位序插入,比如说有5个元素,插入到第三个元素的位置
注意在有头结点时,位序5,意味着是结点6
假如我们要插入的位序是3,意味着我们要寻找的是位序2,也就是结点3,当j=i-1时我们跳出循环,先操作,后j++,j代表当前结点值从0开始,也就是我们在j=3的时候应该跳出循环,所以先操作,后j++,就是j<i-1,j=i的时候就跳出循环

传入什么? 表+插入位置+插入的值
分为几步?
首先是非法操作的判断,是否合法.
第二步是,寻找要插入的位置,插入第几个位置,就找到他前一个位置即i-1,让此时的指针p落在该点处,即我们可以操作他的next域
第三步,先判断吐过p指向空,插入操作不合法,若合法,分配堆空间给一个新的结点s,s的数据域是传入值e,s的指针域指向原先的i(i-1的next域,即p当前的next域),然后将i-1的next域指向新的i

核心思想:先连后断
bool ListInsert(LinkList L,int i,int e)
{
   
    if(i<1)
        return false;
    LNode *p=L;  //为什么需要p指针,因为我们不能动表头指针
    int j=0;    //用来判断当前指针在第几个结点处,j=0,意思是在头结点处
    while(p!=NULL&&j<i-1) //P不能为空,为空咋插入啊,操作不合法,i=j的时候跳出循环
    {
   
        p=p->next; 
        j++; 
    }    //通过这个循环,我们就能找到A的指向B的next域,在他俩中间插入C
    if(p==NULL)
        return false;  //上个循环判断它是否为空,为空不执行,为空的具体操作写在了这,为空就结束
    LNode *s=(LNode *)malloc(sizeof(LNode));
    s->data=e;
    s->next=p->next;
    p->next=s;
    return true; 
}

3.1.2 在指定结点的前后插入

一.后插操作

分两步
判断操作是否合法(p指针是否为空+s是否能分配)
插入元素操作

InsertNextNode(LNode *p,int 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; 
}

二.前插操作

前插操作我们这里不讨论从前遍历一遍,到最后的那种方法
而是考虑,用后插法再交换他们的数据域这种形式可以将时间复杂度降低到o(1)

bool InsertPriorNode(LNode *p,int 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->data=p->data;
    p->data=e;
    return true; 
}

4.单链表的删除

4.1 按位序删除

第一步与之前的查找的相同的,现查找位序-1的点
然后再进行删除操作

bool ListDelete(LinkList L,int i,int &e)
{
   
    if(i<1)
        return false;
    LNode *p=L;  //为什么需要p指针,因为我们不能动L头指针
    int j=0;  //用来判断当前指针在第几个结点处,j=0,意思是在头结点处
    while(p!=NULL&&j<i-1) //P不能为空,为空咋插入啊,操作不合法,i=j的时候跳出循环
    {
   
        p=p->next; 
        j++; 
    } 
    if(p==NULL) 
        return false;
    if(p->next==NULL) 
        return false;
    LNode *q=p->next;  //方便操作搞出了一个q,直接用p也行,就是写起来不直观
    e=q->data;
    p->next=q->next;
    free(q);
    return true; 
}

4.2 指定结点的删除

指定删除结点p,我们思考,给你结点p删除它,需要找到前一个结点,但是那样做太麻烦了,不如交换指定结点和后一个结点的数据域,再删除新的后继结点

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

5.单链表的查找

5.1 按位序查找

返回值是位序结点的指针

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

5.2 按值查找

LNode * LocateElem(LinkList L,int e)
{
   
    LNode *p=L->next; //从第一个结点处开始查值
    while(p!=NULL&&p->data!=e)
    {
   
        p=p->next;
    }
    return p; 
}

补充一个:求表的长度

int Length(LinkList L){
   
    int len=0;  //不包括头结点 
    LNode *p=L;
    while(p->next!=NULL)
    {
   
        p=p->next;
        len++;
    }
    return len;
}

6. 单链表的建立(带头结点的建立)

单链表的建立包括了头结点的建立(初始化)

6.1 尾插法建立单链表

- 在尾插法中,LNode *s,*r=L;这个写法,其实是为了简化代码,实际上*s不需要赋值,
- 因为在接下来的代码中会给结点s分配堆空间,结点s的位置就会变成随机的,
- 实际上,我们只需要让r=L就行,声明一个s即可
  • 声明输入值x,分配头结点,声明s和r指针
  • 循环分配s结点再把它加入链表,再循环的输入x值
  • 链表尾指针置空
LinkList List_Tailnsert(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; 
}

6.2 头插法建立单链表

  • 头插法相比于尾插法,我们要把头指针置空,因为分配的头指针很可能指向神秘的空间有脏数据
LinkList List_Headlnsert(LinkList L)
{
   
    int x;
    L=(LinkList)malloc(sizeof(LNode));
    L->next=NULL; //初始链表头指针指向NULL
    LNode *s;
    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; 
}

本文完整代码

#include <stdio.h>
#include <stdlib.h>
typedef struct LNode{
   
    int data;
    struct LNode *next;
}LNode ,*LinkList;

void InitList() {
   
    LinkList L;//定义头指针变量 
    L=(LNode*)malloc(sizeof(LNode));//头指针指向分配的头结点内存空间 
    L->next=NULL;
    return true;
}
bool ListInsert(LinkList L,int i,int e)
{
   
    if(i<1)
        return false;
    LNode *p=L;  //为什么需要p指针,因为我们不能动L头指针
    int j=0;  //用来判断当前指针在第几个结点处,j=0,意思是在头结点处
    while(p!=NULL&&j<i-1) //P不能为空,为空咋插入啊,操作不合法,i=j的时候跳出循环
    {
   
        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; 
}

InsertNextNode(LNode *p,int 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; 
}
bool InsertPriorNode(LNode *p,int 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->data=p->data;
    p->data=e;
    return true; 
}

bool ListDelete(LinkList L,int i,int &e)
{
   
    if(i<1)
        return false;
    LNode *p=L;  //为什么需要p指针,因为我们不能动L头指针
    int j=0;  //用来判断当前指针在第几个结点处,j=0,意思是在头结点处
    while(p!=NULL&&j<i-1) //P不能为空,为空咋插入啊,操作不合法,i=j的时候跳出循环
    {
   
        p=p->next; 
        j++; 
    } 
    if(p==NULL) 
        return false;
    if(p->next==NULL) 
        return false;
    LNode *q=p->next;  //方便操作搞出了一个q,直接用p也行,就是写起来不直观
    e=q->data;
    p->next=q->next;
    free(q);
    return true; 
}

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

LNode * GetElem(LinkList L,int i)
{
   
    if(i<0)
        return NULL;
    LNode *p=L;
    int j=0;
    while(p!=NULL&&j<i)
    {
   
        p=p->next;
        j++;
    }
    return p;
} 
LNode * LocateElem(LinkList L,int e)
{
   
    LNode *p=L->next; //从第一个结点处开始查值
    while(p!=NULL&&p->data!=e)
    {
   
        p=p->next;
    }
    return p; 
}

int Length(LinkList L){
   
    int len=0;  //不包括头结点 
    LNode *p=L;
    while(p->next!=NULL)
    {
   
        p=p->next;
        len++;
    }
    return len;
}
LinkList List_Tailnsert(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_Headlnsert(LinkList L)
{
   
    int x;
    L=(LinkList)malloc(sizeof(LNode));
    LNode *s;
    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; 
}

int main()
{
   
    InitList();
    int e=-1;
}
相关文章
|
2月前
|
搜索推荐 C语言
数据结构(C语言)之对归并排序的介绍与理解
归并排序是一种基于分治策略的排序算法,通过递归将数组不断分割为子数组,直到每个子数组仅剩一个元素,再逐步合并这些有序的子数组以得到最终的有序数组。递归版本中,每次分割区间为[left, mid]和[mid+1, right],确保每两个区间内数据有序后进行合并。非递归版本则通过逐步增加gap值(初始为1),先对单个元素排序,再逐步扩大到更大的区间进行合并,直至整个数组有序。归并排序的时间复杂度为O(n*logn),空间复杂度为O(n),且具有稳定性,适用于普通排序及大文件排序场景。
|
3月前
|
存储 网络协议 编译器
【C语言】深入解析C语言结构体:定义、声明与高级应用实践
通过根据需求合理选择结构体定义和声明的放置位置,并灵活结合动态内存分配、内存优化和数据结构设计,可以显著提高代码的可维护性和运行效率。在实际开发中,建议遵循以下原则: - **模块化设计**:尽可能封装实现细节,减少模块间的耦合。 - **内存管理**:明确动态分配与释放的责任,防止资源泄漏。 - **优化顺序**:合理排列结构体成员以减少内存占用。
215 14
|
3月前
|
C语言
【C语言】全局搜索变量却找不到定义?原来是因为宏!
使用条件编译和 `extern` 来管理全局变量的定义和声明是一种有效的技术,但应谨慎使用。在可能的情况下,应该优先考虑使用局部变量、函数参数和返回值、静态变量或者更高级的封装技术(如结构体和类)来减少全局变量的使用。
56 5
|
3月前
|
编译器 C语言
【C语言】宏定义在 a.c 中定义,如何在 b.c 中使用?
通过将宏定义放在头文件 `macros.h` 中,并在多个源文件中包含该头文件,我们能够在多个文件中共享宏定义。这种方法不仅提高了代码的重用性和一致性,还简化了维护和管理工作。本文通过具体示例展示了如何定义和使用宏定义,帮助读者更好地理解和应用宏定义的机制。
124 2
|
4月前
|
存储 算法 C语言
C语言中常见的字符串处理技巧,包括字符串的定义、初始化、输入输出、长度计算、比较、查找与替换、拼接、截取、转换、遍历及注意事项
本文深入探讨了C语言中常见的字符串处理技巧,包括字符串的定义、初始化、输入输出、长度计算、比较、查找与替换、拼接、截取、转换、遍历及注意事项,并通过案例分析展示了实际应用,旨在帮助读者提高编程效率和代码质量。
198 4
|
4月前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
85 4
|
4月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
164 16
|
4月前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
228 8
|
4月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
64 1
|
2月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
158 77