紫薇星上的数据结构(3)

链表与顺序表一样,也是线性表的一种,今天就接着上次线性表中的知识点来整理链表。


3.1初识链表

基本概念

链表由N个结点链组成,第一个结点储存的位置叫头指针,最后一个节点的指针为“空”,节点包括数据域和指针域。和昨天我们讲的顺序表有些差异,这是我们昨天的定义:

我们随便举个例子:

typedef struct Monster{
	int id;
	char *name;

     //指向下一个节点
	struct Monster *next; 
}Monster;

这样就可以实现链表结构了,我们来尝试写一个看一下:

typedef struct Monster{
	int id;
	char *name;
	struct Monster *next; 
}Monster;

void test(){
	Moster monster1 = {1, "史莱克"};
	Moster monster2 = {2, "哥斯拉"};
	Moster monster3 = {3, "黑熊精"};
	Moster monster4 = {4, "金角大王"};
	Moster monster5 = {5, "银角大王"};
	
	//monster1就是头指针
	monster1.next = &monster2;
	monster2.next = &monster3;
	monster3.next = &monster4;
	monster4.next = &monster5;
	monster5.next = NULL;
}

链表相较于顺序表的优点:

  • 不用存储时规定长度
  • 存储的元素不受限制
  • 插入和删除元素时,不用移动其他元素

链表的头指针和头结点

头指针:链表中第一个结点的储存位置;

头结点:在单链表的第一个结点前附设的一个结点。

两者异同
头指针头结点

若链表有头结点,这指向头结点的指针;

若没有则是链表指向第一个结点的指针。

头节点是为了操作和统一方便设立的,放在第一个结点之前;

其数据域一般无意义(可存储链表长度)。

头指针具有表示作用,所以经常使用头指针表示链表名字。有了头结点,在第一个结点前插入,删除第一个结点的时候,操作与其他结点就统一了。

无论链表是否为空,头指针必定不为空;

头指针是链表的必要元素。

头节点不一定是链表的必要元素。

3.2单链表

单链表基本概念

链表中每一个结点中只包含一个指针域:

typedef struct Monster{
	int id;
	char *name;

     //指向下一个节点
	struct Monster *next; 
}Monster;

​

单链表的插入

要在第 i 个结点后插入一个结点时,需要以下步骤:

  • 创建一个空结点,分配内存空间,设置内存数据;
  • 获取第 i 个结点,设置新结点的后继结点为该结点的后继结点;
  • 设置第 i 个结点的后继结点为新结点。

这时候我们使用代码来实现一下插入算法,别忘了初始化链表,我们在昨天的工程中先新建一个LinkList.h,然后编写如下代码:

#ifndef LINKLIST_H_INCLUDED
#define LINKLIST_H_INCLUDED

#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"

/**定义链表的结点,包含数据域和指针域*/
typedef struct Node{
    ElementType data; //数据域
    struct Node *next; //指针域,指向下个结点
}Node;

/**定义头结点,以便于统一链表结点的插入与删除操作*/
typedef struct LinkList{
    Node *next; //头指针(如果链表有头节点,就指向头结点;没有就指向第一个结点)
    int length; // 链表的长度初始为零
}LinkList;

/**初始化链表*/
void InitLinkList(LinkList *linkList, ElementType *dataArray, int length);

/**在指定位置pos处插入元素element,这里注意是指定位置,不是下标*/
void InsertLinkList(LinkList *linkList, int pos, ElementType element);

/**再写一个Print*/
void PrintLinkList(LinkList *linkList);

#endif // LINKLIST_H_INCLUDED

这里要注意,在链表定义的时候我们同时定义好了头结点LinkList,这样就方便处理链表结点的插入与删除了。现在我们来实现一下,首先新建一个.c文件LinkList.c:

#include <stdio.h>
#include <stdlib.h>
#include "LinkList.h"

/**初始化链表*/
void InitLinkList(LinkList *linkList, ElementType *dataArray, int length){

}

/**在指定位置pos处插入元素element,这里注意是指定位置,不是下标*/
void InsertLinkList(LinkList *linkList, int pos, ElementType element){

}

/**再写一个Print*/
void PrintLinkList(LinkList *linkList){

}

然后编写代码:

#include "LinkList.h"

/**初始化链表*/
void InitLinkList(LinkList *linkList, ElementType *dataArray, int length){
    for(int i = 0; i < length; i++){
        InsertLinkList(linkList, i + 1, dataArray[i]);
    }
}

/**在指定位置pos处插入元素element,这里注意是指定位置,不是下标*/
void InsertLinkList(LinkList *linkList, int pos, ElementType element){
   //1、创建空结点并为数据域赋值
    Node *node = (Node*)malloc(sizeof(Node));
    node->data = element;
    node->next = NULL;
    //2、找到要插入的节点
    if(pos == 1){ //插入的是第一个元素
        linkList->next = node;
        node->next = linkList->next;
        linkList->length ++;
        return;
    }
    //如果不是,需要通过循环找到要插入的位置
    Node *currNode = linkList->next;
    for(int i = 1; currNode && i < pos - 1; i++){
        currNode = currNode->next;
    }
    //3、将结点插入并对接前面的结点
    if(currNode){
        node->next = currNode->next;
        currNode->next = node;
        linkList->length ++;
    }
}

/**再写一个Print*/
void PrintLinkList(LinkList *linkList){
    Node *node = linkList->next;
    if(!node){
        printf("链表为空!\n");
        linkList->length = 0;
        return;
    }
    for(int i = 0; i < linkList->length; i++){
        printf("%d\t%s\n", node->data.id, node->data.name);
        node = node->next;
    }
}

这里要注意我们在循环找到插入位置的时候需要用一个新的指针currNode,同时要保证currNode不为空,这时我们的插入算法就写好了,我们在main.c文件中实现一下:

#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
#include "LinkList.h"

ElementType dataArray[] = {
    {1, "钢铁侠"},
    {2, "美国队长"},
    {3, "紫薇一号"},
    {4, "紫薇二号"},
    {5, "紫薇三号"}
};

void TestLinkList();

int main(){
    TestLinkList();
    return 0;
}

void TestLinkList(){
    LinkList linkList;
    linkList.length = 0;
    InitLinkList(&linkList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
    printf("插入前:\n");
    PrintLinkList(&linkList);
    ElementType element;
    element.id = 11;
    element.name = (char*)malloc(10);
    strcpy(element.name, "新增者");
    InsertLinkList(&linkList, 2, element);
    printf("插入后:\n");
    PrintLinkList(&linkList);
}

我们一定要h记得在初始化之后将长度设为零,编译通过,运行结果如下:

插入前:
1       钢铁侠
2       美国队长
3       紫薇一号
4       紫薇二号
5       紫薇三号
插入后:
1       钢铁侠
11      新增者
2       美国队长
3       紫薇一号
4       紫薇二号
5       紫薇三号

Process returned 0 (0x0)   execution time : 0.029 s
Press any key to continue.

这就是插入的全部过程。

单链表的查找

要获取第 i 个结点的数据,我们需要以下步骤:

  • 声明一个结点指针P,让其指向链表的第一个结点a1,初始化一个变量 j ;
  • 当 j < i 时,遍历链表,让P的指针向后移动,不断指向下一个结点,同时 j 累加 1 ;
  • 当链表末尾P为空时,这说明第 i 个元素不存在;否则查找成功,返回结点P的数据。

同时我们还可以将昨天整理的几个辅助方法都写进去,是否为空,得到长度都写进去,首先在LinkList.h中添加操作:

/**判断是否为空*/
int IsLinkEmpty(LinkList *linkList);

/**返回链表大小*/
int GetLinkLength(LinkList *linkList);

/**得到pos位置的元素*/
ElementType GetLinkListElement(LinkList *linkList, int pos);

然后在LinkList.c中添加操作:

/**判断是否为空*/
int IsLinkEmpty(LinkList *linkList){
    return (GetLinkLength(&linkList) == 0 ? 1 : 0);
}

/**返回链表大小*/
int GetLinkLength(LinkList *linkList){
    if(linkList->length == NULL){
        return 0;
    }
    return linkList->length;
}

/**得到pos位置的元素*/
ElementType GetLinkListElement(LinkList *linkList, int pos){
    Node *node = linkList->next;
    for(int i = 1; node && i < pos; i++){
        node = node->next;
    }
    return node->data;
}

这时候在main.c中实现一下:

#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
#include "LinkList.h"

ElementType dataArray[] = {
    {1, "钢铁侠"},
    {2, "美国队长"},
    {3, "紫薇一号"},
    {4, "紫薇二号"},
    {5, "紫薇三号"}
};

void TestLinkList();

int main(){
    TestLinkList();
    return 0;
}

void TestLinkList(){
    LinkList linkList;
    linkList.length = 0;
    InitLinkList(&linkList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
    printf("插入前:\n");
    PrintLinkList(&linkList);
    ElementType element;
    element.id = 11;
    element.name = (char*)malloc(10);
    strcpy(element.name, "新增者");
    InsertLinkList(&linkList, 2, element);
    printf("插入后:\n");
    PrintLinkList(&linkList);
    printf("链表是否为空:%d,长度为:%d\n",IsLinkEmpty(&linkList), GetLinkLength(&linkList));
    printf("寻找第三个元素:%d\t%s\n", GetLinkListElement(&linkList, 3).id, GetLinkListElement(&linkList, 3).name);
}

编译通过,运行结果如下:

插入前:
1       钢铁侠
2       美国队长
3       紫薇一号
4       紫薇二号
5       紫薇三号
插入后:
1       钢铁侠
11      新增者
2       美国队长
3       紫薇一号
4       紫薇二号
5       紫薇三号
链表是否为空:0,长度为:6
寻找第三个元素:2       美国队长

Process returned 0 (0x0)   execution time : 0.028 s
Press any key to continue.

单链表的删除

要删除第 i 个结点的数据,我们要进行以下步骤:

  • 获取第 i 个结点,若该结点不是第一个结点,则获取第 i - 1 个结点;
  • 将第 i - 1 个结点的后缀结点设为第一个结点的后缀结点;
  • 删除第 i 个结点,并释放内存空间,记录并返回删除的数据元素的值。

首先我们在 LinkList.h 中添加操作:

/**删除pos位置上的结点*/
ElementType DeleteLinkListElement(LinkList *linkList, int pos);

然后在LinkList.c中添加操作:

/**删除pos位置上的结点*/
ElementType DeleteLinkListElement(LinkList *linkList, int pos){
    ElementType element; //被删除的元素
    element.id = -999; //用一个不可能的值来判断是否成功
    Node *node = NULL;
    if(pos == 1){ //如果是第一个结点
        node = linkList->next;
        if(node){
            element = node->data;
            linkList->next = node->next;
            free(node);
            linkList->length --;
        }
        return element;
    }
    //如果不是第一个结点
    //1、找到要删除结点和他的前缀节点
    //2、要删除的结点的next赋值给前缀结点的next
    //3、释放找到的结点
    Node *preNode; //前缀结点
    node = linkList->next;
    for(int i = 1; node && i < pos; i++){
        preNode = node;
        node = node->next;
    }
    if(node){
        element = node->data;
        preNode->next = node->next;
        free(node);
        linkList->length --;
    }
}

这里要注意,如果删除的是第一个结点,那么直接将第一个结点的next给LinkList的next,然后free就可以了;如果不是第一个结点,那么就要使用前缀结点来查找结点,查找到后将删除结点的next给前缀结点的next,然后free。我们在main.c中实现一下:

void TestLinkList(){
    LinkList linkList;
    linkList.length = 0;
    InitLinkList(&linkList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
    printf("插入前:\n");
    PrintLinkList(&linkList);
    ElementType element;
    element.id = 11;
    element.name = (char*)malloc(10);
    strcpy(element.name, "新增者");
    InsertLinkList(&linkList, 2, element);
    printf("插入后:\n");
    PrintLinkList(&linkList);
    printf("链表是否为空:%d,长度为:%d\n",IsLinkEmpty(&linkList), GetLinkLength(&linkList));
    printf("寻找第三个元素:%d\t%s\n", GetLinkListElement(&linkList, 3).id, GetLinkListElement(&linkList, 3).name);
    printf("删除第三个元素:\n");
    DeleteLinkListElement(&linkList, 3);
    PrintLinkList(&linkList);
}

编译通过,运行结果如下:

插入前:
1       钢铁侠
2       美国队长
3       紫薇一号
4       紫薇二号
5       紫薇三号
插入后:
1       钢铁侠
11      新增者
2       美国队长
3       紫薇一号
4       紫薇二号
5       紫薇三号
链表是否为空:0,长度为:6
寻找第三个元素:2       美国队长
删除第三个元素:
1       钢铁侠
11      新增者
3       紫薇一号
4       紫薇二号
5       紫薇三号

Process returned 0 (0x0)   execution time : 0.035 s
Press any key to continue.

单链表的清空

如何清空单链表呢?大家可能会觉得先顺序表一样直接将长度变为0就行了,但是链表是很多个不同地址的数据域与指针域连接起来的,所以还要释放内存空间,而且要一个一个释放,所以下面这种写法是是错误的:

/**清除链表*/
void ClearLinkList(LinkList *linkList){
    if(linkList == NULL){
        return;
    }
    linkList->length = 0;
    free(linkList);
}

这样子乍一看很符合大家的想法,其实只不过是将长度的数值变为零了,然后释放了头结点的内存空间罢了,后面的内存空间都没有释放,所以正确清除链表的步骤如下:

  • 声明结点P、Q;
  • 将第一个结点赋值给P;
  • 循环将下一个结点赋值给Q,然后释放P,再将Q赋值给P。

我们首先在LinkList.h中进行操作:

/**清除链表*/
void ClearLinkList(LinkList *linkList);

然后在LinkList.c中进行操作:

/**清除链表*/
void ClearLinkList(LinkList *linkList){
    Node *node = linkList->next;
    Node *nextNode;
    while(node){
        nextNode = node->next; //先记录当前节点的下个结点,以便释放当前结点
        free(node);
        node = nextNode;
    }
    linkList->next = NULL;
    linkList->length = 0;
}

接下来我们实现一下:

void TestLinkList(){
    LinkList linkList;
    linkList.length = 0;
    InitLinkList(&linkList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
    printf("插入前:\n");
    PrintLinkList(&linkList);
    ElementType element;
    element.id = 11;
    element.name = (char*)malloc(10);
    strcpy(element.name, "新增者");
    InsertLinkList(&linkList, 2, element);
    printf("插入后:\n");
    PrintLinkList(&linkList);
    printf("链表是否为空:%d,长度为:%d\n",IsLinkEmpty(&linkList), GetLinkLength(&linkList));
    printf("寻找第三个元素:%d\t%s\n", GetLinkListElement(&linkList, 3).id, GetLinkListElement(&linkList, 3).name);
    printf("删除第三个元素:\n");
    DeleteLinkListElement(&linkList, 3);
    PrintLinkList(&linkList);
    printf("清除链表!打印如下:\n");
    ClearLinkList(&linkList);
    PrintLinkList(&linkList);
}

编译通过,运行结果如下:

插入前:
1       钢铁侠
2       美国队长
3       紫薇一号
4       紫薇二号
5       紫薇三号
插入后:
1       钢铁侠
11      新增者
2       美国队长
3       紫薇一号
4       紫薇二号
5       紫薇三号
链表是否为空:0,长度为:6
寻找第三个元素:2       美国队长
删除第三个元素:
1       钢铁侠
11      新增者
3       紫薇一号
4       紫薇二号
5       紫薇三号
清除链表!打印如下:
链表为空!

Process returned 0 (0x0)   execution time : 0.065 s
Press any key to continue.

单链表与顺序表的对比

3.3循环链表

循环链表的结构与单链表完全相同,唯一有一点不同的是尾结点不是指向NULL,而是指向第一个结点,这里要注意,带有头结点的循环链表,尾结点的指针域依然指向第一个结点,而不是头结点。

我们先新建一个头文件CircularList.h,在里面定义循环链表与操作:

#ifndef CIRCULARLIST_H_INCLUDED
#define CIRCULARLIST_H_INCLUDED

#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"

/**定义链表的结点,包含数据域和指针域*/
typedef struct CircularNode{
    ElementType data; //数据域
    struct Node *next; //指针域,指向下个结点
}CircularNode;

/**定义头结点,以便于统一链表结点的插入与删除操作*/
typedef struct CircularLinkList{
    CircularNode *next; //指向第一个结点
    int length; // 链表的长度初始为零
}CircularLinkList;

/**初始化链表*/
void InitCricularLinkList(CircularLinkList *clList, ElementType *dataArray, int length);

/**在指定位置pos处插入元素element,这里注意是指定位置,不是下标*/
void InsertCircularLinkList(CircularLinkList *clList, int pos, ElementType element);

/**再写一个Print*/
void PrintCircularLinkList(CircularLinkList *clList);

/**删除pos位置上的结点*/
ElementType DeleteCircularLinkListElement(CircularLinkList *clList, int pos);

/**根据元素内容返回对应的结点指针*/
CircularNode *GetCircularLinkListNode(CircularLinkList *clList, ElementType element);

/**根据结点 来遍历*/
void PrintCircularLinkListByNode(CircularLinkList *clList, CircularNode *node);

#endif // CIRCULARLIST_H_INCLUDED

这里我们就主要说一下几个比较重要的操作,比如插入、删除、遍历这些操作,其他的操作大同小异,大家可以自己实现一下。

循环链表的插入

这里我们要知道,循环链表的最大特点就是尾指针指向第一个结点,所以当插入的是第一个位置的时候有两种情况:

  • 如果插入时链表长度为零:那么就要将插入元素的指针域指向自身;
  • 如果插入时链表长度不为零:那么就要将List的next变为插入的元素,然后将最后一位元素的指针域指向插入元素。

其他的时候插入我们与单链表插入无异。

现在我们在CricularList.c的文件中编写操作:

#include "CircularList.h"

/**初始化链表*/
void InitCricularLinkList(CircularLinkList *clList, ElementType *dataArray, int length){
     for(int i = 0; i < length; i++){
        InsertLinkList(clList, i + 1, dataArray[i]);
    }
}

/**在指定位置pos处插入元素element,这里注意是指定位置,不是下标*/
void InsertCircularLinkList(CircularLinkList *clList, int pos, ElementType element){
    //创建一个新结点
    CircularNode *node = (CircularNode*)malloc(sizeof(CircularNode));
    node->data = element;
    node->next = NULL;
    if(pos == 1){
        node->next = clList->next;
        if(!node->next){
            node->next = node;
        }else{
            //长度不为零
            CircularNode *lastNode = clList->next;
            for(int i = 1; i < clList->length; i++){
                lastNode = lastNode->next;
            }
            lastNode->next = node;
        }
        clList->next = node;
        clList->length ++;
        return;
    }
    //插入的不是第一个结点,需要通过循环找到要插入的位置
    CircularNode *currNode = clList->next;
    for(int i = 1; currNode && i < pos - 1; i++){
        currNode = currNode->next;
    }
    //将结点插入并对接前面的结点
    if(currNode){
        node->next = currNode->next;
        currNode->next = node;
        clList->length ++;
        if(pos == clList->length){
            node->next = clList->next;
        }
    }
}

/**再写一个Print*/
void PrintCircularLinkList(CircularLinkList *clList){
    if(clList->length == 0 || !clList->next){
        printf("链表为空!\n");
        clList->length = 0;
        return;
    }
    CircularNode *node = clList->next;
    for(int i = 0; i < clList->length; i++){
        printf("%d\t%s\n", node->data.id, node->data.name);
        node = node->next;
    }
}

然后我们在main.c中实现一下:

#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
#include "LinkList.h"
#include "CircularList.h"

ElementType dataArray[] = {
    {1, "钢铁侠"},
    {2, "美国队长"},
    {3, "紫薇一号"},
    {4, "紫薇二号"},
    {5, "紫薇三号"}
};

void TestCircularList();

int main(){
    TestCircularList();
    return 0;
}

void TestCircularList(){
    CircularLinkList *clList = (CircularLinkList*)malloc(sizeof(CircularLinkList));
    clList->length = 0;
    clList->next = NULL;
    InitCricularLinkList(clList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
    PrintCircularLinkList(clList);
}

简单的执行一下,编译通过,运行结果如下:

1       钢铁侠
2       美国队长
3       紫薇一号
4       紫薇二号
5       紫薇三号

Process returned 0 (0x0)   execution time : 0.043 s
Press any key to continue.

循环链表的删除

在删除的时候我们也要考虑在删除第一个结点的时候,需要将最后一个结点的next先给到第一个元素的next,然后删除,同样的,删除其他的结点与单链表相同。在CricularList.c的文件中编写操作:

/**删除pos位置上的结点*/
ElementType DeleteCircularLinkListElement(CircularLinkList *clList, int pos){
    ElementType element; //被删除的元素
    element.id = -999; //用一个不可能的值来判断是否成功
    if(pos == 1){ //如果是第一个结点
        CircularNode *node = clList->next;
        if(node){
            element = node->data;
            //找到最后一个元素,该百年其指针域的指向
            CircularNode *lastNode = clList->next;
            for(int i = 1; i < clList->length; i++){
                lastNode = lastNode->next;
            }
            clList->next = node->next;
            lastNode->next = clList->next;
            free(node);
            clList->length --;
        }
        return;
    }
    CircularNode *preNode; //前缀结点
    CircularNode *node = clList->next;
    for(int i = 1; node && i < pos; i++){
        preNode = node;
        node = node->next;
    }
    if(node){
        element = node->data;
        preNode->next = node->next;
        free(node);
        clList->length --;
    }
    return element;
}

然后在main.c中实现一下:

void TestCircularList(){
    CircularLinkList *clList = (CircularLinkList*)malloc(sizeof(CircularLinkList));
    clList->length = 0;
    clList->next = NULL;
    InitCricularLinkList(clList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
    PrintCircularLinkList(clList);
    printf("删除第三个元素:\n");
    DeleteCircularLinkListElement(clList, 2);
    PrintCircularLinkList(clList);
}

编译通过,运行结果如下:

1       钢铁侠
2       美国队长
3       紫薇一号
4       紫薇二号
5       紫薇三号
删除第三个元素:
1       钢铁侠
3       紫薇一号
4       紫薇二号
5       紫薇三号

Process returned 0 (0x0)   execution time : 0.028 s
Press any key to continue.

循环链表的遍历

因为是循环链表,所以我们可以通过任意一个节点来遍历链表,首先我们来写一个根据元素返回对应的结点指针,这样可以方便遍历,先在CricularList.c的文件中编写操作:

/**根据元素内容返回对应的结点指针*/
CircularNode *GetCircularLinkListNode(CircularLinkList *clList, ElementType element){
    CircularNode *node = clList->next;
    if(!node){
        return NULL;
    }
    //使用循环来判断现在的结点与初始节点是否相等
    do{//因为第一次判断是相等的
        if(element.id == node->data.id && strcmp(element.name, node->data.name) == 0){
            return node;
        }
        node = node->next;
    }while(node != clList->next);
    return NULL;
}

然后我们再根据这个节点返回的指针来遍历:

/**根据结点 来遍历*/
void PrintCircularLinkListByNode(CircularLinkList *clList, CircularNode *node){
    if(!node){
        printf("链表为空!\n");
        return;
    }
    if(!clList->next){
        printf("链表为空!\n");
        return;
    }
    //记录初始指针
    CircularNode *origNode = node;
    do{
      printf("%d\t%s\n", node->data.id, node->data.name);
      node = node->next;
    }while(node != origNode);
}

现在我们在main.c中实现一下:

void TestCircularList(){
    CircularLinkList *clList = (CircularLinkList*)malloc(sizeof(CircularLinkList));
    clList->length = 0;
    clList->next = NULL;
    InitCricularLinkList(clList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
    PrintCircularLinkList(clList);
    printf("删除第三个元素:\n");
    DeleteCircularLinkListElement(clList, 2);
    PrintCircularLinkList(clList);
    printf("从元素:紫薇一号 开始遍历:\n");
    ElementType element;
    element.id = 3;
    element.name = "紫薇一号";
    CircularNode *node = GetCircularLinkListNode(clList, element);
    PrintCircularLinkListByNode(clList, node);
}

编译通过,运行结果如下:

1       钢铁侠
2       美国队长
3       紫薇一号
4       紫薇二号
5       紫薇三号
删除第三个元素:
1       钢铁侠
3       紫薇一号
4       紫薇二号
5       紫薇三号
从元素:紫薇一号 开始遍历:
3       紫薇一号
4       紫薇二号
5       紫薇三号
1       钢铁侠

Process returned 0 (0x0)   execution time : 0.036 s
Press any key to continue.

3.4静态链表

用数组描述的链表,即称为静态链表,在C语言中,静态链表的表现形式就是结构体数组,结构体变量包括数据域与游标。

优点:

  • 在插入与删除时只需要改变游标,不需要修改元素;
  • 还有优秀的思考方式。

缺点:

  • 没有解决空间动态分配长度的问题;
  • 与顺序表相比,并没有带来本质的效率提升。

我们来举一个例子看一下:

typedef struct{
    DataElement data; 
    int next; 
}StaticLinkList[10];

现在我们就定义了一个静态链表,在它的表现形式中,有数据与与游标,就像数据域与指针域一样,不同的是:

  • 数组的第一个元素的next游标用来存放第一个空闲结点的下标;
  • 数组的最后一个元素next用来存放第一个插入元素的下标,初始时为0;
  • 数组的第一个元素与最后一个元素不能使用;
  • 初始化时,每个元素的next都指向下一个元素。
大概长这个样子

这时候如果我们向静态链表中插入一个元素data:

  • 这个元素首先被保存在第二个数组下标的数据域中;
  • 然后由于下标为2的数据域中有了数据,所以现在第一个空闲结点的下标为3;
  • 而由于2中有了数据,其他数据域都没有数据,那么它的游标就不能指向别人,变为了0;
  • 下标为9的结点由于要存放第一个插入元素的下标,next变为1;
  • 那么8的next就应该变为0。

这样子看起来是不是非常的烧脑呢哈哈哈哈,我们来定义一下试试看:

静态链表的插入

首先建立Static.h与Static.c,在Static.h中编写:

#ifndef STATICLIST_H_INCLUDED
#define STATICLIST_H_INCLUDED

#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"

#define MAX_SIZE_SSL 10
#define OK 1
#define ERROR 0

typedef struct{
    ElementType data; //数据域
    int next;  //游标,如果为0则无指向
}StaticLinkList[MAX_SIZE_SSL];

//初始化静态链表
void InitStaticLinkList(StaticLinkList slList);

//在pos位置进行插入
int InsertStaticLinkList(StaticLinkList slList, int pos, ElementType element);

//为静态链表分配内存
int mallocSSL(StaticLinkList slList);

//获得静态链表的长度
int GetStaticLinkList(StaticLinkList slList);

//打印方法
void PrintStaticLinkList(StaticLinkList slList);

#endif // STATICLIST_H_INCLUDED

然后在Static.c中编写:

#include "StaticList.h"

void InitStaticLinkList(StaticLinkList slList){
    for(int i = 0; i < MAX_SIZE_SSL; i++){
        slList[i].next = i + 1;
    }
    //将最后一个节点置空
    slList[MAX_SIZE_SSL - 1].next = 0;
    slList[MAX_SIZE_SSL - 2].next = 0;
}

//打印方法
void PrintStaticLinkList(StaticLinkList slList){
    for(int i = 0; i< MAX_SIZE_SSL; i++){
        printf("i:%d\tnext:%d\n",i,slList[i].next);
    }
}

现在我们尝试一下在main.c中打印一个静态链表:

#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
#include "LinkList.h"
#include "CircularList.h"
#include "StaticList.h"

ElementType dataArray[] = {
    {1, "钢铁侠"},
    {2, "美国队长"},
    {3, "紫薇一号"},
    {4, "紫薇二号"},
    {5, "紫薇三号"}
};

void TestSequenceList();

void TestLinkList();

void TestCircularList();

void TestStaticList();

int main(){
    //printf("Hello world!\n");
    //TestSequenceList();
    //TestLinkList();
    //TestCircularList();
    TestStaticList();
    return 0;
}

void TestStaticList(){
    StaticLinkList slList;
    InitStaticLinkList(slList);
    PrintStaticLinkList(slList);
}

编译通过,运行结果为:

i:0     next:1
i:1     next:2
i:2     next:3
i:3     next:4
i:4     next:5
i:5     next:6
i:6     next:7
i:7     next:8
i:8     next:0
i:9     next:0

Process returned 0 (0x0)   execution time : 0.052 s
Press any key to continue.

现在我们来讨论插入的问题,首先我们模拟一个场景:

这是一个有数据的静态链表

现在我们假设要往2号位置插入一个数据element,那么要进行的过程是这样的:

首先通过第一个元素游标获得空结点下标5;

本来插入后5号结点的游标就变成了0,同时4号结点的next就变成了5;

但现在因为要插入到2号结点,所以将5的next变为2号结点的next:3,同时将2号结点的next变为5;

现在就变成了:

插入之后的静态链表

其实这种插入方法与单链表的插入方法大同小异,只不过因为静态链表是数组类型,数据结点不能随意更改位置,所以需要画图来说明。

我们在StaticList.c中编写操作:

#include "StaticList.h"

void InitStaticLinkList(StaticLinkList slList){
    for(int i = 0; i < MAX_SIZE_SSL; i++){
        slList[i].data.id = -99;
        slList[i].data.name = "NULL";
        slList[i].next = i + 1;
    }
    //将最后一个节点置空
    slList[MAX_SIZE_SSL - 1].next = 0;
    slList[MAX_SIZE_SSL - 2].next = 0;
}

//在pos位置进行插入
int InsertStaticLinkList(StaticLinkList slList, int pos, ElementType element){
    if(pos < 1 || pos > GetStaticLinkList(slList) + 1){
        return ERROR;
    }
    int cursor = MAX_SIZE_SSL - 1;
    //需要判断cursor范围是否合法
    //分配内存
    int newIndex = mallocSSL(slList);
    if(newIndex){
        slList[newIndex].data = element;
        //找到newIndex的前缀结点
        for(int i = 1; i <= pos - 1; i++){
            cursor = slList[cursor].next;
        }
    }
    slList[newIndex].next = slList[cursor].next;
    slList[cursor].next = newIndex;
}

//为静态链表分配内存
int mallocSSL(StaticLinkList slList){
    //拿到第一个空闲结点下标(备用链表下标)
    int cursor = slList[0].next;
    if(cursor){
        slList[0].next = slList[cursor].next;
    }
    return cursor;
}

//获得静态链表的长度
int GetStaticLinkList(StaticLinkList slList){
    int count = 0;
    int cursor = slList[MAX_SIZE_SSL - 1].next;
    while(cursor){
        //p = p->next
        cursor = slList[cursor].next;
        count ++;
    }
    return count;
}

//打印方法
void PrintStaticLinkList(StaticLinkList slList){
    for(int i = 0; i< MAX_SIZE_SSL; i++){
        printf("i:%d\tnext:%d\tid:%d\tname:%s\n", i, slList[i].next, slList[i].data.id, slList[i].data.name);
    }
}

我们通过malloc方法来给新增元素结点进行操作,然后插入,稍微修改了一下输出方式和初始化方式,这样看起来就比较直接了,我们在main.c文件中实现一下插入:

void TestStaticList(){
    StaticLinkList slList;
    InitStaticLinkList(slList);
    PrintStaticLinkList(slList);
    ElementType element;
    element.id = 11;
    element.name = "小丑";
    InsertStaticLinkList(slList, 1, element);
    printf("插入元素后:\n");
    PrintStaticLinkList(slList);
}

编译通过,运行结果为:

i:0     next:1  id:-99  name:NULL
i:1     next:2  id:-99  name:NULL
i:2     next:3  id:-99  name:NULL
i:3     next:4  id:-99  name:NULL
i:4     next:5  id:-99  name:NULL
i:5     next:6  id:-99  name:NULL
i:6     next:7  id:-99  name:NULL
i:7     next:8  id:-99  name:NULL
i:8     next:0  id:-99  name:NULL
i:9     next:0  id:-99  name:NULL
插入元素后:
i:0     next:2  id:-99  name:NULL
i:1     next:0  id:11   name:小丑
i:2     next:3  id:-99  name:NULL
i:3     next:4  id:-99  name:NULL
i:4     next:5  id:-99  name:NULL
i:5     next:6  id:-99  name:NULL
i:6     next:7  id:-99  name:NULL
i:7     next:8  id:-99  name:NULL
i:8     next:0  id:-99  name:NULL
i:9     next:1  id:-99  name:NULL

Process returned 0 (0x0)   execution time : 0.062 s
Press any key to continue.

这样就可以看到,我们插入之后数据被保存在了第二个元素的数据域内,同时最后一个元素的游标也改变了,符合我们静态链表的特征。

静态链表的删除

我们首先在Static.h中添加操作:

//删除pos位置的元素
int DeleteStaticLinkList(StaticLinkList slList, int pos);

//回收原始数组中的指定下标index位置的空间
void FreeStaticLinkList(StaticLinkList slList, int index);

然后在Static.c中添加操作:

//删除pos位置的元素
int DeleteStaticLinkList(StaticLinkList slList, int pos){
     if(pos < 1 || pos > GetStaticLinkList(slList) + 1){
        return ERROR;
    }
    int cursor = MAX_SIZE_SSL - 1;
    //找到newIndex的前缀结点
    for(int i = 1; i <= pos - 1; i++){
            cursor = slList[cursor].next;
    }
    int delIndex = slList[cursor].next;
    slList[cursor].next = slList[delIndex].next;
    FreeStaticLinkList(slList, delIndex);
    return OK;
}

//回收原始数组中的指定下标index位置的空间
void FreeStaticLinkList(StaticLinkList slList, int index){
    //将下标为index的空闲结点回收到备用链表
    slList[index].next = slList[0].next;
    //0号元素的next结点指向备用链表的第一个结点,表示index的节点空闲
    slList[0].next = index;
}

这样就能删除了,我们多插入几个元素试一下:

void TestStaticList(){
    StaticLinkList slList;
    InitStaticLinkList(slList);
    PrintStaticLinkList(slList);
    ElementType element1;
    element1.id = 11;
    element1.name = "小丑";
    InsertStaticLinkList(slList, 1, element1);
    ElementType element2;
    element2.id = 12;
    element2.name = "蝙蝠侠";
    InsertStaticLinkList(slList, 2, element2);
    ElementType element3;
    element3.id = 13;
    element3.name = "蜘蛛侠";
    InsertStaticLinkList(slList, 3, element3);
    printf("插入元素后:\n");
    PrintStaticLinkList(slList);
    printf("删除第二个后:\n");
    DeleteStaticLinkList(slList, 2);
    PrintStaticLinkList(slList);
}

编译通过,运行结果如下:

i:0     next:1  id:-99  name:NULL
i:1     next:2  id:-99  name:NULL
i:2     next:3  id:-99  name:NULL
i:3     next:4  id:-99  name:NULL
i:4     next:5  id:-99  name:NULL
i:5     next:6  id:-99  name:NULL
i:6     next:7  id:-99  name:NULL
i:7     next:8  id:-99  name:NULL
i:8     next:0  id:-99  name:NULL
i:9     next:0  id:-99  name:NULL
插入元素后:
i:0     next:4  id:-99  name:NULL
i:1     next:2  id:11   name:小丑
i:2     next:3  id:12   name:蝙蝠侠
i:3     next:0  id:13   name:蜘蛛侠
i:4     next:5  id:-99  name:NULL
i:5     next:6  id:-99  name:NULL
i:6     next:7  id:-99  name:NULL
i:7     next:8  id:-99  name:NULL
i:8     next:0  id:-99  name:NULL
i:9     next:1  id:-99  name:NULL
删除第二个后:
i:0     next:2  id:-99  name:NULL
i:1     next:3  id:11   name:小丑
i:2     next:4  id:12   name:蝙蝠侠
i:3     next:0  id:13   name:蜘蛛侠
i:4     next:5  id:-99  name:NULL
i:5     next:6  id:-99  name:NULL
i:6     next:7  id:-99  name:NULL
i:7     next:8  id:-99  name:NULL
i:8     next:0  id:-99  name:NULL
i:9     next:1  id:-99  name:NULL

Process returned 0 (0x0)   execution time : 0.077 s
Press any key to continue.

这里可以看到,下标为零的游标变为2,所以2号就是空闲的结点,已经被删除了,这里我们看不到被删除,但只要再添加一个元素,就会从2号开始添加,所以就可以理解为2号元素被删除了。

3.5双向链表

双向链表就是在原有链表的基础上添加了一个前置指针,所以在指向的时候一个元素的前置指针必须指向前面的元素,后继指针必须指向它后面的元素,同时它前面的元素的后继指针必须指向它,它后面的元素的前置指针也必须指向它。同时因为他有前置指针,所以它也可以是循环链表。

双向链表插入

我们依旧来看一下如何实现插画与删除,老样子,我们先建立一个DoublyLinkList.h与DoublyLinkList.c的文件,然后在.h的文件中添加定义和操作:

#ifndef DOUBLYLINKLIST_H_INCLUDED
#define DOUBLYLINKLIST_H_INCLUDED

#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"

typedef struct DoublyNode{
    ElementType data; //数据域
    struct DoublyNode *prev; //指向前缀结点
    struct DoublyNode *next; //指向后继结点
}DoublyNode;

typedef struct DoublyLinkList{
    int length;
    DoublyNode *next;
};

//插入元素
void InsertDoublyLinkList(DoublyLinkList *dlList, int pos, ElementType element);

//打印内容
void PrintDoublyLinkList(DoublyLinkList *dlList);

#endif // DOUBLYLINKLIST_H_INCLUDED

这时候我们就要分析一下,在插入的时候,我们要将插入元素的前置指针指向被插入的元素之前的元素上,后继指针要指向被插入元素,同时被插入元素的前置指针要指向插入元素,被插入元素的前一个元素的后继指针要指向插入元素。说的有点绕口哈,我们来看一下操作,在.c文件中添加操作:

#include "DoublyLinkList.h"

//插入元素
void InsertDoublyLinkList(DoublyLinkList *dlList, int pos, ElementType element){
    //创建空结点
    DoublyNode *node = (DoublyNode*)malloc(sizeof(DoublyNode));
    node->data = element;
    node->prev = NULL;
    node->next = NULL;
    //在第一个位置插入节点
    if(pos == 1){
        if(dlList->length == 0){
            dlList->next = node;
            dlList->length ++;
            return;
        }
        node->next = dlList->next;
        dlList->next = node;
        node->next->prev = node;
        dlList->length ++;
        return;
    }
    DoublyNode *currNode = dlList->next;
    for(int i = 1; currNode && i < pos - 1; i++){
        currNode = currNode->next;
    }
    if(currNode){
        node->prev = currNode;
        if(currNode->next){//如果前缀结点非空(空就表示没有后继结点了)
            //将插入位置处的前置指针指向新的结点
            currNode->next->prev = node;
        }
        node->next = currNode->next;
        currNode->next = node;
        dlList->length ++;
    }
}

//打印内容
void PrintDoublyLinkList(DoublyLinkList *dlList){
    DoublyNode *node = dlList->next;
    if(!node || dlList->length == 0){
        printf("链表为空!\n");
        dlList->length = 0;
        return;
    }
     for(int i = 0; i < dlList->length; i++){
        printf("%d\t%s\n", node->data.id, node->data.name);
        node = node->next;
    }
}

在main.c中实现一下:

#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
#include "LinkList.h"
#include "CircularList.h"
#include "StaticList.h"
#include "DoublyLinkList.h"
ElementType dataArray[] = {
    {1, "钢铁侠"},
    {2, "美国队长"},
    {3, "紫薇一号"},
    {4, "紫薇二号"},
    {5, "紫薇三号"}
};

void TestSequenceList();

void TestLinkList();

void TestCircularList();

void TestStaticList();

void TestDoublyList();

int main(){
    //printf("Hello world!\n");
    //TestSequenceList();
    //TestLinkList();
    //TestCircularList();
    //TestStaticList();
    TestDoublyList();
    return 0;
}

void TestDoublyList(){
    DoublyLinkList *dlList = (DoublyLinkList*)malloc(sizeof(DoublyLinkList));
    dlList->length = 0;
    dlList->next = NULL;
    InsertDoublyLinkList(dlList, 1, dataArray[0]);
    InsertDoublyLinkList(dlList, 2, dataArray[1]);
    PrintDoublyLinkList(dlList);
}

插入两个元素,编译通过,运行结果如下:

1       钢铁侠
2       美国队长

Process returned 0 (0x0)   execution time : 0.019 s
Press any key to continue.

双向链表删除

删除与插入相同,都要考虑双指针的问题,首先在.h的文件中添加操作:

//删除pos位置的元素
ElementType DeleteDoublyLinkList(DoublyLinkList *dlList, int pos);

在.h中实现操作:

//删除pos位置的元素
ElementType DeleteDoublyLinkList(DoublyLinkList *dlList, int pos){
    ElementType element;
    element.id = -999;
    if(pos == 1){
        DoublyNode *node = dlList->next;
        if(node){
            element = node->data;
            dlList->next = node->next;
            if(node->next){
                //如果有第二个结点,那么设置第二个节点的前缀为NULL
                node->next->prev = NULL;
            }
            free(node);
            dlList->length --;
        }
        return element;
    }
    DoublyNode *node = dlList->next;
    for(int i = 1; node && i < pos; i++){
        node = node->next;
    }
    if(node){
        element = node->data;
        if(node->next){
            node->next->prev = node->prev;
        }
        node->prev->next = node->prev;
        free(node);
        dlList->length --;
    }
    return element;
}

在main.c文件中实现一下:

void TestDoublyList(){
    DoublyLinkList *dlList = (DoublyLinkList*)malloc(sizeof(DoublyLinkList));
    dlList->length = 0;
    dlList->next = NULL;
    InsertDoublyLinkList(dlList, 1, dataArray[0]);
    InsertDoublyLinkList(dlList, 2, dataArray[1]);
    PrintDoublyLinkList(dlList);
    printf("删除第二个位置的元素:\n");
    DeleteDoublyLinkList(dlList, 2);
    PrintDoublyLinkList(dlList);
}

编译通过,运行结果如下:

1       钢铁侠
2       美国队长
删除第二个位置的元素:
1       钢铁侠

Process returned 0 (0x0)   execution time : 0.029 s
Press any key to continue.

3.6链表的比较


这一部分整理了关于链表的知识点,确实有很多,今天整理的挺晚了,下次会整理栈的知识点,我们下次见👋

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值