文章目录

  • 链式存储
  • 单链表
  • 函数声明
  • 双链表
  • 函数的声明


链式存储

上一节课学习了顺序表存储,知道了其工作原理和优缺点,下面将开始学习另一种存储方式:链式存储,俗称链表。

顾名思义,链表就是用一个类似于"链条"的东西将数据一个一个连接起来。所以可以得知,它是由两部分组成,一部分为数据域(存放数据的地方),另一部分为指针域(“链条”)。

简单的介绍了一下链表的含义及组成,可以发现,它跟顺序表有很多区别:

  1. 顺序表存储数据是开辟一整块大的空间,按(下标)顺序进行分配空间,也就是每个数据之间的地址是连续的。而链表却恰好相反,它是每次使用时随机开辟一小块空间(一个数据域和指针域的空间),所以其相邻数据之间的地址不是连续的,而是随机的。
  2. 顺序表查询速度快、操作表(插入、删除)速度慢;而链表因为数据之间地址随机,只能根据一个节点中的指针域去查询到下一个数据,所以查询速度慢(时间复杂度为O(N)),而它在插入和删除的速度很快(忽略查询到目标位置的时间),其时间复杂度为O(1),所以对于一些需要频繁操作表数据的存储情况,使用链表存储的效率要高于顺序表。

由此可以看出,链表的优缺点也一目了然,查询速度慢,但操作速度(插入、删除)快。

下面,将用代码实现链表:

单链表

单链表也是最简单且最经典的链式结构,其组成结构也是最简单的,只有一个数据域和一个指向下一个数据域的指针域。

代码实现

函数声明

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct Node{
    int val;//数据域
    struct Node *pNext;//指向下一个数据的指针
}node,*pNode;//node是该结构体的名称,而pNode是该结构体的指针
pNode init();//初始化链表
bool add(pNode head,int len);//添加数据,len是添加长度
bool isEmpty(pNode head);//判断链表是否为空
bool insert(pNode head,int position,int val);//在position插入数据,val是插入的值
int length(pNode head);//求出链表的长度
int find_for_position(pNode head,int position);//查询position位置上的值,返回该节点的值
pNode find_for_val(pNode head,int val);//查询并返回值为val的节点
void traverse(pNode head);//遍历链表
bool delete_for_position(pNode head,int position);//删除指定位置的节点
bool delete_for_val(pNode head,int val);//删除值为val的节点
void close(pNode head);//关闭链表,释放空间
int main(){
    return 0;
}

下面将对上述函数进行实现并测试

初始化链表

pNode init(){
    pNode head = (pNode) malloc(sizeof (node));//动态创建一个头结点
    if(head==NULL){//如果空间分配失败,则强制退出程序
        printf("分配失败,程序终止!\n");
        exit(-1);
    }
    return head;
}

添加数据

单链表的添加操作一般使用的是尾插法。

链表如何存mysql 链表文件存储_链表

bool add(pNode head,int len){
    pNode temp = head;//用一个指针指向头结点,方便进行下面的操作
    for(int i = 0;i<len;i++){
        int val = 0;//需要添加的数据
        printf("请输入需要添加的值:\n");
        scanf("%d",&val);
        pNode pnew = (pNode) malloc(sizeof (node));
        if(pnew==NULL){//如果空间分配失败,则强制退出程序
            printf("空间分配失败!\n");
            return false;
        }
        pnew->val = val;
        temp->pNext = pnew;
        pnew->pNext = NULL;//将新添节点进行悬挂
        temp = pnew;//temp指向新生节点,方便后续继续添加
    }
    return true;
}

遍历链表(先实现该函数,方便后续DeBug)

void traverse(pNode head){
    pNode temp = head->pNext;//因为头结点并不参与存储数据,所以要从头结点的下一个节点开始遍历
    while(temp!=NULL){
        printf("%d ",temp->val);
        temp = temp->pNext;
    }
    printf("\n");
}

到此已经实现了三个函数,先编写main函数进行测试一下

int main(){
    pNode head = init();
    int n;
    printf("请输入需要添加节点个数:\n");
    scanf("%d",&n);
    if(!add(head,n)){
        printf("添加失败!\n");
    }else{
        traverse(head);
    }
    return 0;
}

测试结果为:

链表如何存mysql 链表文件存储_数据结构_02

说明上面的几个函数代码实现没有问题,接下来,实现其他函数。

链表长度

int length(pNode head){
    int num = 0;
    pNode temp = head -> pNext;
    while(temp != NULL){
        temp = temp -> pNext;
        num++;
    }
    return num;
}

判断链表是否为空

bool isEmpty(pNode head){
    if(head->pNext!=NULL||head!=NULL){
        return false;
    }
    return true;
}

插入数据

bool insert(pNode head,int position,int val){
    pNode temp = head;
    int len = length(head);//求出链表长度
    if(position<0||temp==NULL) {
        return false;
    }
    pNode pnew  = (pNode)malloc(sizeof(node));
    pnew->val = val;
    if(pnew==NULL){
        printf("动态分配内存失败!\n");
        return false;
    }
    if(position>=len){//如果插入位置超出链表长度,则直接添加在现链表尾部
        while(temp -> pNext != NULL){
            temp = temp -> pNext;
        }
    }else{
        for(int i = 0;i<position-1;i++) {
            temp = temp->pNext;
        }
    }
    pnew->pNext = temp->pNext;
    temp->pNext = pnew;
    return true;
}

到此,又实现了几个函数,接下来再进行测试

int main(){
    pNode head = init();
    int n;
    printf("请输入需要添加节点个数:\n");
    scanf("%d",&n);
    if(!add(head,n)){
        printf("添加失败!\n");
    }else{
        printf("插入数据前:");
        traverse(head);
    }
    printf("该链表长度为:%d\n", length(head));
    if(isEmpty(head)){
        printf("链表为空\n");
    }else{
        printf("链表不为空\n");
    }
    insert(head,0,4);//在位置0(头节点)处插入4
    printf("插入数据后:");
    traverse(head);
    return 0;
}

运行结果为:

链表如何存mysql 链表文件存储_插入节点_03

接下来是实现查询函数,其中包含了两个查询,一个是按位置查询,一个是按节点中数值的值查询并返回该节点

按指定位置查询

int find_for_position(pNode head,int position){
    int len = length(head);
    if(position<0||position>len){
        //如果位置非法,则返回-1
        return -1;
    }
    //循环到position节点
    for(int i = 0;i<position;i++){
        head = head->pNext;
    }
    //找到则返回该节点的值
    return head->val;
}

按值查询

pNode find_for_val(pNode head,int val){
    while(head!=NULL){
        if(head->val==val){
            return head;
        }
        head = head -> pNext;
    }
    return NULL;
}

进行函数功能的测试

int main(){
    pNode head = init();
    int n;
    printf("请输入需要添加节点个数:\n");
    scanf("%d",&n);
    if(!add(head,n)){
        printf("添加失败!\n");
    }
    printf("该节点的值为:%d\n",find_for_position(head,4));
    printf("该节点的值为:%d\n",find_for_position(head,3));
    int num;
    printf("请输入要查询的值:\n");
    scanf("%d",&num);
    pNode temp = find_for_val(head, num);
    if(temp!=NULL){
        printf("该节点的值为:%d\n",temp->val);
    }else{
        printf("NULL\n");
    }
    return 0;
}

测试结果为:

链表如何存mysql 链表文件存储_链表_04

最后是删除结点的功能

删除目标值的结点

bool delete_for_val(pNode head,int val){
    int i=0;
    pNode ptemp = head;
    while(ptemp->pNext!=NULL&&ptemp->pNext->val!=val){
        ptemp = ptemp->pNext;
        i++;
    }
    if(ptemp->pNext==NULL) {
        printf("抱歉,删除失败!\n");
        return false;
    }
    pNode p = ptemp->pNext;
    ptemp->pNext = ptemp->pNext->pNext;
    free(p);
    p = NULL;//释放删除结点的内存
    printf("成功删除位置%d的数据%d\n",i,val);
    return true;
}

删除指定位置的结点

bool delete_for_position(pNode head,int position){
    int len = length(head);//求出链表的长度
    pNode temp = head;
    if(position>len||position<0||len==0){
        printf("删除失败!");
        return false;
    }
    for(int i=0;i<position-1;i++){
        temp = temp->pNext;
    }
    pNode p = temp->pNext;
    temp->pNext = temp->pNext->pNext;
    free(p);//释放删除结点的内存
    p = NULL;
    printf("删除成功!\n");
    return true;
}

删除功能也实现完毕,最后,实现关闭链表的函数

void close(pNode head){
    head = NULL;
    free(head);
}

所有函数全部实现,接下来进行测试删除结点函数

int main(){
    pNode head = init();
    int n;
    printf("请输入需要添加节点个数:\n");
    scanf("%d",&n);
    if(!add(head,n)){
        printf("添加失败!\n");
    }else{
        printf("插入数据前:");
        traverse(head);
    }
    printf("该链表长度为:%d\n", length(head));
    if(isEmpty(head)){
        printf("链表为空\n");
    }else{
        printf("链表不为空\n");
    }
    insert(head,0,4);
    printf("插入数据后:");
    traverse(head);
    delete_for_val(head,4);//删除值为4的结点
    delete_for_position(head,1);//删除位置1的结点
    printf("删除数据后:");
    traverse(head);
    close(head);//最后要关闭链表
    return 0;
}

运行结果:

链表如何存mysql 链表文件存储_数据_05

到此,一个简单的单链表的全部功能已经实现。

双链表

双链表是指拥有一个数据域和两个指针域的链式存储结构,其中的两个指针域,一个指向前节点,另一个指向下一个节点。

代码实现

函数的声明

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct Node{
    int val;//数据域
    struct Node *pBefore;//指向前一个数据的指针
    struct Node *pNext;//指向下一个数据的指针
}node,*pNode;//node是该结构体的名称,而pNode是该结构体的指针
pNode init();//初始化链表
bool order_insert(pNode head,pNode tail);//头插法,从头节点插入节点
bool disorder_insert(pNode head,pNode tail);//尾插法,从链表的末尾插入节点
bool isEmpty(pNode head,pNode tail);//判断链表是否为空
int length(pNode head);//求出链表的长度
void traverse(pNode head,pNode tail);//顺序遍历链表
void reverse(pNode head,pNode tail);//逆序遍历链表
int find_for_position(pNode head,pNode tail,int position);//查询position位置上的值,返回该节点的值
pNode find_for_val(pNode head,pNode tail,int val);//查询并返回值为val的节点
bool delete_for_position(pNode head,pNode tail,int position);//删除指定位置的节点
bool delete_for_val(pNode head,int val);//删除值为val的节点
void close(pNode head,pNode tail);//关闭链表,释放空间

从函数的声明可以看出,双链表比单链表多了一种插入方式(头插法),逆序遍历,其实这些东西单链表也可以实现,但相比之下双链表实现这些功能更加方便,也更加实用。

下面将从链表的初始化、插入节点、查询、删除等方式分类实现这些函数

初始化链表

双链表的初始化和单链表的初始化一样

pNode init(){
    pNode Node = (pNode)malloc(sizeof(node));
    return Node;
}

判断链表是否为空

bool isEmpty(pNode head,pNode tail){
    if(head==NULL||tail==NULL||head==tail){
        return true;
    }
    return false;
}

求出链表的长度

int length(pNode head,pNode tail){
    int num = 0;
    pNode temp = head -> pNext;
    while(temp != NULL&& temp != tail){
        temp = temp -> pNext;
        num++;
    }
    return num;
}

插入节点

头插法

bool order_insert(pNode head,pNode tail){
    //插入节点的个数
    int len;
    //创建一个临时节点替代tail
    pNode temp = tail;
    printf("请输入插入节点个数:\n");
    scanf("%d",&len);
    for(int i=0;i<len;i++){
        pNode pnew = (pNode)malloc(sizeof(node));
        if(pnew == NULL){
            printf("内存分配失败!\n");
            return false;
        }
        printf("请输入插入的值:\n");
        scanf("%d",&pnew->val);
        pnew->pNext = temp;
        temp->pBefore = pnew;
        pnew->pBefore = NULL;
        temp = pnew;
    }
    head->pNext = temp;
    temp->pBefore = head;
    return true;
}

尾插法

bool disorder_insert(pNode head,pNode tail){
    //插入节点的个数
    int len;
    //创建一个临时节点替代head
    pNode temp = head;
    printf("请输入插入节点个数:\n");
    scanf("%d",&len);
    for(int i = 0;i<len;i++){
        pNode pnew = (pNode)malloc(sizeof(node));
        if(pnew == NULL){
            printf("内存分配失败!\n");
            return false;
        }
        printf("请输入插入的值:\n");
        scanf("%d",&pnew->val);
        temp->pNext = pnew;
        pnew->pBefore = temp;
        pnew->pNext = NULL;
        temp = pnew;
    }
    temp->pNext = tail;
    tail->pBefore = temp;
    return true;
}

这两种插入方式,原理如下图所示

链表如何存mysql 链表文件存储_插入节点_06

查询

根据节点位置查询

int find_for_position(pNode head,pNode tail,int position){
    int len = length(head,tail);
    if(position<0||position>len){
        //如果位置非法,则返回-1
        return -1;
    }
    //创建一个临时节点
    pNode temp;
    if(position>=len/2){//如果查询的位置大于链表一半的长度,则从尾开始查询效率更高
        temp = tail->pBefore;
        for(int i = len;i>position;i--){
            temp = temp->pBefore;
        }
    }else{//否则从头开始找
        temp = head->pNext;
        for(int i = 0;i<position;i++){
            temp = temp->pNext;
        }
    }
    //找到则返回该节点的值
    return temp->val;
}

根据值查询,因为无法知道值的大致位置,所以这个函数实现方式和单链表一致,都是从头开始查询。

pNode find_for_val(pNode head,int val){
    while(head!=NULL){
        if(head->val==val){
            return head;
        }
        head = head -> pNext;
    }
    return NULL;
}

删除

根据位置删除,与根据位置查询有异曲同工之处

bool delete_for_position(pNode head,pNode tail,int position){
    int len = length(head,tail);//求出链表的长度
    if(position>len||position<0||len==0){
        printf("删除失败!");
        return false;
    }
    pNode temp;//创建临时节点
    if(position>=len/2){//如果查询的位置大于链表一半的长度,则从尾开始查询效率更高
        temp = tail->pBefore;
        for(int i = len;i>position;i--){
            temp = temp->pBefore;
        }
        temp = temp -> pBefore;
    }else{//否则从头开始找
        temp = head->pNext;
        for(int i = 0;i<position-1;i++){
            temp = temp->pNext;
        }
    }
    pNode p = temp->pNext;
    temp->pNext = temp->pNext->pNext;
    temp->pNext->pBefore = temp;
    free(p);//释放删除结点的内存
    p = NULL;
    printf("删除成功!\n");
    return true;
}

根据值删除

bool delete_for_val(pNode head,int val){
    int i=0;
    pNode ptemp = head;
    while(ptemp->pNext!=NULL&&ptemp->pNext->val!=val){
        ptemp = ptemp->pNext;
        i++;
    }
    if(ptemp->pNext==NULL) {
        printf("抱歉,删除失败!\n");
        return false;
    }
    pNode p = ptemp->pNext;
    ptemp->pNext = ptemp->pNext->pNext;
    free(p);
    p = NULL;//释放删除结点的内存
    printf("成功删除位置%d的数据%d\n",i,val);
    return true;
}

关闭链表

void close(pNode head,pNode tail){
    head = NULL;
    free(head);
    tail = NULL;
    free(tail);
}

此时,所有函数全部已经实现了,开始测试

int main(){
    //初始化头指针和尾指针
    pNode head = init();
    pNode tail = init();
    //头尾指针互相链接
    head->pNext = tail;
    head->pBefore = NULL;
    tail->pNext = NULL;
    tail->pBefore = head;
    disorder_insert(head,tail);//尾插法
    traverse(head,tail);//顺序遍历
    reverse(head,tail);//逆序遍历
    delete_for_position(head,tail,2);
    delete_for_val(head, 4);
    traverse(head,tail);
    close(head,tail);
    return 0;
}

运行结果如下:

链表如何存mysql 链表文件存储_数据结构_07


这便是双链表的基本功能,对比单链表,它在查询数据时多了一种从尾开始查询,这样提高了查询的效率。