数据结构—单链表结构的实现与分析

文章向导

线性表的链式存储结构
创建一个单链表
删除一个单链表
查找、插入、删除链表结点
一个完整的实例,验证成果!

一、线性表的链式存储结构
  线性表的链式存储结构就是用一组任意的存储单元存储线性表中的元素,这组存储单元可以是连续的,也可以是不连续的。同时也就意味着这些数据元素可以存在于内存未被占用的任意位置。
  我们习惯将链表(链式表)中的每个元素称之为结点,每个节点包含两部分的组成内容:存储数据元素的信息(数据域) + 存储直接后继(下一个结点)的位置(指针域)。有点懵?希望下面两张图能帮到你!
这里写图片描述
这里写图片描述
  上图中清晰地描述了链式存储结构中结点间的逻辑关系,说到这儿其实还应该补充一点小内容,然后就可以正式开始接下来的单链表具体实现。
  对于线性表来说,必然有头有尾。“头”通常用头结点来表示,而“尾”则是链表的最后一个结点(将其指针域置为NULL,用于表明这是尾部)。虽然一些本科数据结构教材中会把头结点称之为是链表中的第一个结点,但这点对于初学者来说很容易在具体的代码实践中造成混淆,所以我的建议是在理解上干脆就把头结点给独立起来,剩下的部分则是第一个结点到最后一个结点。
  这里写图片描述
  


二、创建以及删除一个单链表

  我曾在一些本科数据结构的教材中发现一很有意思的事情,单链表都还没有就已开始论起了单链表的查找、插入、删除结点等操作,最后才来说明如何创建以及删除一个单链表。这点让我很是纳闷,俗话说“巧妇难为无米之炊”,不知看到这儿的读者你认不认同。所以,本文的组织也是遵循着这个原则,同时也是为了后续代码实操时逻辑上的完整性。

1.创建一个单链表

#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
#include<time.h>

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

typedef int ElemType;
typedef int Status; //Status是函数的类型,其值是结果状态代码, 如OK等

typedef struct Node
{
    ElemType data; //数据域
    struct Node *next;  //指针域
} LinkList;

/*********************************************************************/
/* 函数名:CreatListHead
 * 功能:创建一个带头结点的指定结点数目的单向链表
 * 入口参数:
 *      n: 链表结点数目
 *      L: 应传入一维结构体指针自身的地址
 * 返回值:无
*/
void CreatListHead(LinkList **L, int *n)
{
    LinkList *p, *r; 
    int i;

    srand(time(0)); //初始化随机数种子, time(0)为系统时间
    *L = (LinkList*)malloc(sizeof(LinkList)); //*L为整个链表,但形参L为栈变量,注意!!!
    r = *L; //r为指向尾部的结点

    /*将新结点插入表尾:尾插法*/
    for (i = 0; i < *n; i++)
    {
        p = (LinkList*)malloc(sizeof(LinkList)); //生成新结点,总计n个,故在外使用时L->next才是整个链表的第一个节点
        p->data = rand()%100 + 1; //生成[1,100]范围内的随机数
        r->next = p; //表尾结点指向新结点
        r = p;  //将新生成的结点p赋值给r, 让r始终保持为名义上的尾结点
    }
    r->next = NULL; //当前链表结束
}

int main()
{
    LinkList *L = NULL;
    int *n = NULL, tmp = 0;  

    n = &tmp;
    puts("Please Enter the length of linklist.");
    scanf("%d", n);

     //创建一个单链表, 第一个参数不应传入L,因为传入未初始化的L有着极大隐患
    CreatListHead(&L, n);
    return 0;
}

  上述代码的注释详细说明了整个单链表创建时的细节问题,读者可细细研读品尝。但仍有几点值得说明:
  1) main函数中定义的 L为指向头结点的指针变量,理解和使用时可直接等效为头结点。
  2) 为何CreatListHead函数的第一个参数为LinkList **L而不是LinkList *L,有些读者可能会认为第一个参数使用LinkList *L也行啊。但正如注释中提到的,形参本就是一个栈变量,即使main函数传递过来的也是LinkList *L,但malloc分配以及本函数结束后,栈变量就被销毁了,所以仍然于事无补。如果还不明白,可以在创建前后加上打印语句,观察指针L的指向变化。
  3) 应注意到for循环产生了n个结点,而for循环之前的malloc语句则生成了一个头结点给L。

2. 删除一个单链表

  当我们不打算使用一个单链表时,最好将其销毁(也就是在内存中把它释放掉),以便留出空间给其他程序或软件使用。删除整个单链表的思路也很简单,循环释放每个节点即可。
  

#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
#include<time.h>

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

typedef int ElemType;
typedef int Status; //Status是函数的类型,其值是结果状态代码, 如OK等

typedef struct Node
{
    ElemType data;
    struct Node *next;
} LinkList;

/*********************************************************************/
/* 函数名:ClearList
 * 功能:将L重置为空表
 * 入口参数:
 *      L: 应传入一维结构体指针自身的地址
 * 返回值:无
*/
Status ClearList(LinkList **L)
{
    LinkList *p, *q;

    p = (*L)->next; //第一个结点赋值给p
    while(p){
        q = p->next; //下一个结点赋值给q
        free(p); 
        p = q;
    }
    free(*L); //释放头节点
    *L = NULL;
    printf("ClearList: L = %p\n", *L);

return OK;
}

/*********************************************************************/
/* 函数名:CreatListHead
 * 功能:创建一个带头结点的指定结点数目的单向链表
 * 入口参数:
 *      n: 链表结点数目
 *      L: 应传入一维结构体指针自身的地址
 * 返回值:无
*/
void CreatListHead(LinkList **L, int *n)
{
    LinkList *p, *r; 
    int i;

    srand(time(0)); //初始化随机数种子, time(0)为系统时间
    *L = (LinkList*)malloc(sizeof(LinkList)); //*L为整个链表,但形参L为栈变量,注意!!!
    r = *L; //r为指向尾部的结点

    /*将新结点插入表尾:尾插法*/
    for (i = 0; i < *n; i++)
    {
        p = (LinkList*)malloc(sizeof(LinkList)); //生成新结点,总计n个,故在外使用时L->next才是整个链表的第一个节点
        p->data = rand()%100 + 1; //生成[1,100]范围内的随机数
        r->next = p; //表尾结点指向新结点
        r = p;  //将新生成的结点p赋值给r, 让r始终保持为名义上的尾结点
    }
    r->next = NULL; //当前链表结束
}

int main()
{
    LinkList *L = NULL;
    int *n = NULL, tmp = 0;  

    n = &tmp;
    puts("Please Enter the length of linklist.");
    scanf("%d", n);

    //创建一个单链表, 第一个参数不应传入L,因为传入未初始化的L有着极大隐患
    CreatListHead(&L, n); 
    ClearList(&L);

return 0;
}

  看到这儿我们则已经把所谓的米给做好了,接下来则是重点戏部分,Let’s go!


三、查找、插入、删除链表结点

  下面的部分只列出核心代码,整体功能的实现请读者自行将其添加到上面的创建、删除链表程序中。
  
1. 查找链表结点

/* 函数名:GetElem
 * 功能:用e返回L中第get_loc个位置的数据元素值
 * 入口参数:
 *      L: 应传入一维结构体指针
 *      get_loc: 读取的位置
 *      e: 返回读取到的值
 *      n: 链表结点数目
 * 返回值:Status状态码
*/
Status GetElem(LinkList *L, int get_loc, ElemType *e, int *n)
{
    int j; 
    LinkList *p = NULL; //工作指针

    /*输入参数检查*/
     if ( get_loc < 1 || get_loc > *n)
    {
        puts("In GetElem 1");
        return ERROR;
    }

    p = L->next;  //从链表的第一个结点开始
    j = 1; //计数器

    /*非空链表*/
    while ( p && j < get_loc)
    {
        p = p->next; //p指向下一个结点
        ++j;
    }
    printf("p = %p\n", p);
    if ( !p || j > get_loc)
    {
        puts("In GetElem 2");
        return ERROR; //第i个结点不存在
    }
    *e = p->data; //取第i个结点的数据
    return OK;
}

/* 函数名:test_getelem
 * 功能:单链表读取的测试例程
 * 入口参数:
 *      L: 应传入一维结构体指针
 *      n: 链表结点的数目
 * 返回值:无
*/
void test_getelem(LinkList *L, int *n) 
{
    ElemType *e; //待返回的值, 隐含危险:记得初始化
    int get_loc;   //查找的位置
    int tmp;    
    e = &tmp; //初始化至合法区域

    puts("Please Enter value of get_loc:\n");
    scanf("%d", &get_loc);

     /*调用获取方法*/
    if ( GetElem(L, get_loc, e, n) )
    {
        printf("Get node:*e = %d\n", *e); //打印获取到的元素
    }
    else 
    {
        printf("ERROR!\n");
    }
}

  从上述程序片段可得出,单链表结构与顺序存储结构在查找能力上相比,前者为O(n),后者为O(1)。另外,该算法主要的核心思想就是“工作指针后移”,这点也是很多算法中的通用技术。
  

2.插入链表结点
  
  假设想将一结点s插入到结点p和p->next之间,该如何做呢?实际上仅需两条语句:s->next = p->next; p->next = s; 就可实现,不妨看看下图加深理解。
  
这里写图片描述

/***
/* 函数名:ListInsert
 * 功能:在L中第ins_loc个结点位置之前插入新的结点(数据域为e),L的长度加1
 * 入口参数:
 *      L: 应传入一维结构体指针
 *      ins_loc: 待插入的位置
 *      e: 待插入的值
 *      n: 链表结点数目
 * 返回值:Status状态码
*/
Status ListInsert(LinkList *L, int ins_loc, ElemType e, int *n)
{
    int j;
    LinkList *p, *s; //工作指针与待生成的结点
    int length  = *n;

    p = L; //从头节点开始
    j = 1; //结点计数器
    /*输入参数检查*/
     if ( ins_loc < 1 || ins_loc > length)
    {
        puts("In ListInsert 1");
        return ERROR;
    }

    while( p->next && j < ins_loc) /*寻找第ins_loc-1个结点, 因在ins_loc之前插入结点*/
    {
        p = p->next;
        ++j;
    }
    if( !(p->next) || j > ins_loc)  /*第ins_loc个结点不存在*/
    {
        puts("In ListInsert 2");
        return ERROR;
    }
    s = (LinkList*)malloc(sizeof(LinkList)); //生成新结点
    s->data = e;
    s->next = p->next; //将p的后继结点赋值给s的后继
    p->next = s; //将s赋值给P的后继
    length++;
    *n = length;

return OK;
}

/* 函数名:test_listinst
 * 功能:单链表插入结点的测试例程
 * 入口参数:
 *      L: 应传入一维结构体指针
 *  n: 链表结点数目
 * 返回值:无
*/
void test_listinst(LinkList *L, int *n) 
{
    ElemType e; //待插入的值
    int ins_loc;  //待插入的位置

    puts("Please Enter value of ins_loc:\n");
    scanf("%d", &ins_loc);

    puts("Please Enter value of ins_elme:\n");
    scanf("%d", &e);
     /*调用获取方法*/
    if ( ListInsert(L, ins_loc, e, n) )
    {
        printf("insert node: length = %d\n", *n); 
    }
    else 
    {
        printf("ERROR!\n");
    }
}

首先谈谈在第i个位置之前插入结点的算法思路:

  • 声明工作指针p指向链表头结点,初始化j从1开始;
  • 当j < i时让p不断后移指向下一个结点,同时j累加1;
  • 若链表末尾p为NULL,则说明第i个结点不存在;
  • 否则查找成功,并生成一个空结点s;
  • 将e赋值给结点s的数据域;
  • s->next = p->next; p->next = s;
  • 表长加1,返回成功。

      通过上述的步骤分解,读者应该可以顺利理解插入结点的算法思路。但这里需要提及一点针对此类情况的思维优化问题。一些读者习惯于按照计算机的方式机械的代入式分析,这样既难受也效率低下,如果在面对含递归的程序时恐怕会疯掉。
      首先,看一下我们的目标“在i个结点位置之前插入新的结点”,那么这件事两步就可搞定。step1:找到第i-1个结点, step2:利用公式s->next = p->next; p->next = s; 嗯?感觉我在说废话? 我的意思就是不要纠结于如while或for循环真的是不是达成了这件事,而应集中于程序的大框架下某一代码块完成了特定的功能,多个这样的块最后再组合起来即可,这也是算法设计的思考流程。

3.删除链表结点

  假设想将第i个结点从链表中删掉又该如何操作呢?好吧,实际上更为简单仅需一条语句:p->next = p->next->next; 这种关系通过来说明则更加容易让人理解。
  这里写图片描述
  

/********************************************************************/
/* 函数名:ListDelete
 * 功能:删除链表L的第del_loc个结点, 并用e返回其值, 表长减1
 * 入口参数:
 *      L: 应传入一维结构体指针
 *      del_loc: 待删除的位置
 *      e: 待删除结点处的值
 *      n: 链表结点数目
 * 返回值:Status状态码
*/
Status ListDelete(LinkList *L, int del_loc, ElemType *e, int *n)
{
    int j;
    LinkList *p, *q;
    int length = *n;

    p = L; // 从头结点开始
    j = 1;
    /*输入参数检查*/
     if ( del_loc < 1 || del_loc > length)
    {
        puts("In ListDelete 1");
        return ERROR;
    }
    while ( p->next && j < del_loc) //从第一个节点开始判断
    {
         p = p->next;
         ++j;
    }
    if ( !(p->next) || j > del_loc) 
    {
        puts("In ListDelete 2");
        return ERROR;
    }
    q = p->next;
    p->next = q->next; //将q的后继赋值给p的后继
    *e = q->data; //将q结点中的数据给e
    free(q);
    q = NULL;

    length--;
    *n = length;

return OK;
}

/***
/* 函数名:test_listdel
 * 功能:单链表插入结点的测试例程
 * 入口参数:
 *      L: 应传入一维结构体指针
 *      n: 链表结点数目
 * 返回值:无
*/
void test_listdel(LinkList *L, int *n) 
{
    ElemType *e, tmp; //待删除结点的值
    int del_loc;  //待删除的位置

    e = &tmp;   
    puts("Please Enter the value of del_loc:\n");
    scanf("%d", &del_loc);

     /*调用获取方法*/
    if ( ListDelete(L, del_loc, e, n) )
    {
        printf("delete node: *e = %d\n", *e); 
        printf("delete node: length = %d\n", *n); 
    }
    else 
    {
        printf("ERROR!\n");
    }
}

  删除链表结点的算法思路与插入链表结点的算法思路大致类似,都是先找到第i-1个结点,然后进行后续的操作。为何是先找到第i-1个结点呢?emm, 如果你这样问,那么你应该再继续翻到前面链式存储结构处好好阅读下,我就不解释了!
  最后值得说明的是,单链表的插入和删除结点的时间复杂度都是O(n):查找时为O(n),而插入和删除操作上仅是单纯的赋值移动指针而已,时间复杂度为O(1)。故整体算法时间复杂度为O(n)。因此,对于插入或删除数据越频繁的操作,单链表的效率就远大于顺序存储结构。


四、一个完整的实例,验证成果!

  以下是一个完整的单链表测试实例,完成了创建、删除、查找、插入、删除结点这几种基本操作,可以用于验证上述算法的正确性。

#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
#include<time.h>

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

typedef int ElemType;
typedef int Status; //Status是函数的类型,其值是结果状态代码, 如OK等

typedef struct Node
{
    ElemType data;
    struct Node *next;
} LinkList;

/*********************************************************************/
/* 函数名:ListDelete
 * 功能:删除链表L的第del_loc个结点, 并用e返回其值, 表长减1
 * 入口参数:
 *      L: 应传入一维结构体指针
 *      del_loc: 待删除的位置
 *      e: 待删除结点处的值
 *      n: 链表结点数目
 * 返回值:Status状态码
*/
Status ListDelete(LinkList *L, int del_loc, ElemType *e, int *n)
{
    int j;
    LinkList *p, *q;
    int length = *n;

    p = L; // 从头结点开始
    j = 1;
    /*输入参数检查*/
     if ( del_loc < 1 || del_loc > length)
    {
        puts("In ListDelete 1");
        return ERROR;
    }
    while ( p->next && j < del_loc) //从第一个节点开始判断
    {
         p = p->next;
         ++j;
    }
    if ( !(p->next) || j > del_loc) 
    {
        puts("In ListDelete 2");
        return ERROR;
    }
    q = p->next;
    p->next = q->next; //将q的后继赋值给p的后继
    *e = q->data; //将q结点中的数据给e
    free(q);
    q = NULL;

    length--;
    *n = length;

return OK;
}


/*********************************************************************/
/* 函数名:test_listdel
 * 功能:单链表插入结点的测试例程
 * 入口参数:
 *      L: 应传入一维结构体指针
 *      n: 链表结点数目
 * 返回值:无
*/
void test_listdel(LinkList *L, int *n) 
{
    ElemType *e, tmp; //待删除结点的值
    int del_loc;  //待删除的位置

    e = &tmp;   
    puts("Please Enter the value of del_loc:\n");
    scanf("%d", &del_loc);

     /*调用获取方法*/
    if ( ListDelete(L, del_loc, e, n) )
    {
        printf("delete node: *e = %d\n", *e); 
        printf("delete node: length = %d\n", *n); 
    }
    else 
    {
        printf("ERROR!\n");
    }
}


/*********************************************************************/
/* 函数名:ListInsert
 * 功能:单链表插入结点的核心算法
 * 入口参数:
 *      L: 应传入一维结构体指针
 *      ins_loc: 待插入的位置
 *      e: 待插入的值
 *      n: 链表结点数目
 * 返回值:Status状态码
*/
Status ListInsert(LinkList *L, int ins_loc, ElemType e, int *n)
{
    int j;
    LinkList *p, *s; //工作指针与待生成的结点
    int length  = *n;

    p = L; //从头节点开始
    j = 1; //对应第一个结点, 索引标号
    /*输入参数检查*/
     if ( ins_loc < 1 || ins_loc > length)
    {
        puts("In ListInsert 1");
        return ERROR;
    }

    while( p->next && j < ins_loc) /*寻找第ins_loc-1个结点, 因在ins_loc之前插入结点*/
    {
        p = p->next;
        ++j;
    }
    if( !(p->next) || j > ins_loc)  /*第ins_loc个结点不存在*/
    {
        puts("In ListInsert 2");
        return ERROR;
    }
    s = (LinkList*)malloc(sizeof(LinkList)); //生成新结点
    s->data = e;
    s->next = p->next; //将p的后继结点赋值给s的后继
    p->next = s; //将s赋值给P的后继
    length++;
    *n = length;

return OK;
}


/*********************************************************************/
/* 函数名:test_listinst
 * 功能:单链表插入结点的测试例程
 * 入口参数:
 *      L: 应传入一维结构体指针
 *      n: 链表结点数目
 * 返回值:无
*/
void test_listinst(LinkList *L, int *n) 
{
    ElemType e; //待插入的值
    int ins_loc;  //待插入的位置

    puts("Please Enter value of ins_loc:\n");
    scanf("%d", &ins_loc);

    puts("Please Enter value of ins_elme:\n");
    scanf("%d", &e);
     /*调用获取方法*/
    if ( ListInsert(L, ins_loc, e, n) )
    {
        printf("insert node: length = %d\n", *n); 
    }
    else 
    {
        printf("ERROR!\n");
    }
}

/* 函数名:GetElem
 * 功能:用e返回L中第get_loc个位置的数据元素值
 * 入口参数:
 *      L: 应传入一维结构体指针
 *      get_loc: 读取的位置
 *      e: 返回读取到的值
 *      n: 链表结点数目
 * 返回值:Status状态码
*/
Status GetElem(LinkList *L, int get_loc, ElemType *e, int *n)
{
    int j; 
    LinkList *p = NULL; //工作指针

    /*输入参数检查*/
     if ( get_loc < 1 || get_loc > *n)
    {
        puts("In GetElem 1");
        return ERROR;
    }

    p = L->next;  //指向链表中的第一个结点
    j = 1;

    /*非空链表*/
    while ( p && j < get_loc)
    {
        p = p->next; //p指向下一个结点
        j++;
    }
    printf("p = %p\n", p);
    if ( !p || j > get_loc)
    {
        puts("In GetElem 2");
        return ERROR; //第i个结点不存在
    }
    *e = p->data; //取第i个结点的数据
    return OK;
}

/* 函数名:test_getelem
 * 功能:单链表读取的测试例程
 * 入口参数:
 *      L: 应传入一维结构体指针
 *      n: 链表结点的数目
 * 返回值:无
*/
void test_getelem(LinkList *L, int *n) 
{
    ElemType *e; //待返回的值, 隐含危险:记得初始化
    int get_loc;   //查找的位置
    int tmp;    
    e = &tmp; //初始化至合法区域

    puts("Please Enter value of get_loc:\n");
    scanf("%d", &get_loc);

     /*调用获取方法*/
    if ( GetElem(L, get_loc, e, n) )
    {
        printf("Get node:*e = %d\n", *e); //打印获取到的元素
    }
    else 
    {
        printf("ERROR!\n");
    }
}

/* 函数名:ClearList
 * 功能:将L重置为空表
 * 入口参数:
 *      L: 应传入一维结构体指针自身的地址
 * 返回值:无
*/
Status ClearList(LinkList **L)
{
    LinkList *p, *q;

    p = (*L)->next; //第一个结点赋值给p
    while(p){
        q = p->next; //下一个结点赋值给q
        free(p); 
        p = q;
    }
    free(*L); //释放头结点
    *L = NULL;
    printf("ClearList: L = %p\n", *L);

return OK;
}

/* 函数名:CreatListHead
 * 功能:创建一个指定结点数目的单向链表
 * 入口参数:
 *      n: 链表结点数目
 *      L: 应传入一维结构体指针自身的地址
 * 返回值:无
*/
void CreatListHead(LinkList **L, int *n)
{
    LinkList *p, *r; 
    int i;

    srand(time(0)); //初始化随机数种子, time(0)为系统时间
    *L = (LinkList*)malloc(sizeof(LinkList)); //*L为整个链表,L为栈变量注意!!!
    r = *L; //r为指向尾部的结点

    /*将新结点插入表尾:尾插法*/
    for (i = 0; i < *n; i++)
    {
        p = (LinkList*)malloc(sizeof(LinkList)); //生成新结点,总计n个,故在外使用时L->next才是整个链表的第一个节点
        p->data = rand()%100 + 1; //生成[1,100]范围内的随机数
        r->next = p; //表尾结点指向新结点
        r = p;  //将新生成的结点p赋值给r, 让r始终保持为名义上的尾结点
    }
    r->next = NULL; //当前链表结束
}

int main()
{
    LinkList *L = NULL;
    int *n = NULL, tmp = 0;  

    n = &tmp;
    puts("Please Enter the length of linklist.");
    scanf("%d", n);

    CreatListHead(&L, n); //创建一个单链表, 第一个参数不应传入L,因为传入未初始化的L有着极大隐患
    test_getelem(L, n); 
    test_listinst(L, n);
    test_getelem(L, n); 
    test_listdel(L, n);
    test_getelem(L, n); 
    ClearList(&L);

return 0;
}

测试结果:
这里写图片描述

这里写图片描述
  上图的测试很简单,先是获取第一个结点的信息,然后在原链表中第一个结点之前插入新结点(在新链表中新结点则成为第一个结点),然后再获取信息看是否成。接着删除第一个结点,再此获取信息看是否成功。最后释放整个单链表。

参阅资料
《大话数据结构》
《数据结构-C语言版》

猜你喜欢

转载自blog.csdn.net/a574780196/article/details/81413338
今日推荐