线性数据结构---链表

本文为学习数据结构过程中的笔记,内容较为浅显,有问题的评论区见~~ 

一、基本概念

        链表是一种线性数据结构,与数组不同,链表在内存中并不是连续存储的,因此不能通过索引直接访问某个元素。链表由一系列结点组成,每个结点包含两个部分:

        数据域(Data Field):存储实际数据。

        指针域(Pointer Field):存储指向下一个结点的指针。

链表的首个结点被称为“头结点”,最后一个结点被称为“尾结点”。

二、链表的分类

       链表可简单分为单向链表、双向链表和循环链表。

1.概念 

单向链表(Singly Linked List)

        单向链表由一系列结点组成,每个结点包含一个数据域和一个指针域,该指针指向下一个结点。尾结点指向空。

双向链表(Doubly Linked List)

        双向链表的每个结点包含三个部分:数据域、指向下一个结点的指针和指向前一个结点的指针。

循环链表(Circular Linked List)

        循环链表也分为单向循环链表和双向循环链表,其特点是最后一个结点的指针指向链表的头结点,形成一个环,任意结点都可以视为头结点。

2.三种链表的对比

特性

单向链表

双向链表

循环链表

内存占用

较低,只需额外存储一个指针。

较高,每个结点需要存储两个指针。

与相应的单向或双向链表相同。

插入/删除效率

高效,在链表头部插入/删除最为简单。

高效,在任意位置插入/删除都比较简单。

高效,尤其是尾部操作,不需特殊处理尾结点。

访问效率

访问较慢,需要顺序遍历。

比单向链表略快,可双向遍历。

相似于单向或双向链表,需要顺序遍历。

遍历方向

只能从头节点向后遍历。

可以从任意结点向前或向后遍历。

支持循环遍历,从任意节点开始均可遍历整个链表。

实现难度

较低,指针操作相对简单。

较高,需要处理更多指针。

较高,需处理循环指针,防止无限循环。

典型应用场景

实现队列、栈、临时数据存储。

浏览器前进/后退、应用程序的撤销/重做功能、LRU算法。

实时循环任务处理、循环缓冲区。

三、三种链表的代码实现

1.单向链表

(1)结构定义

        数据类型为int型,定义一个指向结构体的指针next用于记录下一个结点的地址。此处为最基础的写法,可以在此基础上扩充其他功能。

// 定义节点结构
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// 创建新节点
Node* createNode(int data);
// 在链表中插入新节点
void insert(Node** head, int data, int position);
// 删除链表中的节点
void deleteNode(Node** head, int position);
// 查找链表中的节点
Node* search(Node* head, int data);
// 打印链表
void printList(Node* head);
// 释放链表内存
void freeList(Node* head);

(2)创建结点

        输入一个int类型数据,先进行动态内存分配来存储这个结点,进行赋值之后将这个结点的next指针置空,然后返回这个结点的地址。

Node* createNode(int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

 (3)插入新结点

        使用一个指向指针的指针Node**head来修改头指针的参数(传址调用),先创建一个新结点newNode,判断position的值,如果插入位置为链表的头部,则直接将新结点的next指向原先的头结点,再把新结点作为头结点。

        否则从头开始遍历链表至position-1,即插入位置的上一个结点。如果插入位置有效,则修改各结点指针的值,将新结点插入。如下图:

void insert(Node** head, int data, int position) {
    Node* newNode = createNode(data);
    if (position == 0) {
        // 插入头部
        newNode->next = *head;
        *head = newNode;
    }
    else {
        Node* temp = *head;
        for (int i = 0; temp != NULL && i < position - 1; i++) {
            temp = temp->next;
        }
        if (temp == NULL) {
            printf("插入位置无效\n");
            free(newNode);
        }
        else {
            // 插入中间或尾部
            newNode->next = temp->next;
            temp->next = newNode;
        }
    }
}

(4)删除链表中的结点

        先判断链表是否为空,接着判断删除位置,如果删除头结点,则直接将头结点的地址改为下一个结点的地址,并释放原来的头结点。

        如果不是头结点则遍历链表,如果删除位置合法,则将删除位置的上一个结点的next指向删除位置的下一个结点,并释放删除位置的内存,即可删除目标位置的结点。如下图:

void deleteNode(Node** head, int position) {
    if (*head == NULL) {
        printf("链表为空\n");
        return;
    }

    Node* temp = *head;

    if (position == 0) {
        // 删除头部节点
        *head = temp->next;
        free(temp);
    }
    else {
        Node* prev = NULL;
        for (int i = 0; temp != NULL && i < position; i++) {
            prev = temp;
            temp = temp->next;
        }
        if (temp == NULL) {
            printf("删除位置无效\n");
        }
        else {
            // 删除中间或尾部节点
            prev->next = temp->next;
            free(temp);
        }
    }
}

(5)查找链表中的结点

        此处采用按照data值的方式查找,遍历链表,找到相关值则返回该结点的地址,也可以采用其他方式,如返回链表中的某一个结点。

Node* search(Node* head, int data) {
    Node* temp = head;
    while (temp != NULL) {
        if (temp->data == data) {
            return temp;
        }
        temp = temp->next;
    }
    return NULL;
}

(6)打印链表

        从头遍历并输出。

void printList(Node* head) {
    Node* temp = head;
    while (temp != NULL) {
        printf("%d -> ", temp->data);
        temp = temp->next;
    }
    printf("NULL\n");
}

(7)释放内存

        从头遍历并释放每个结点。

void freeList(Node* head) {
    Node* temp;
    while (head != NULL) {
        temp = head;
        head = head->next;
        free(temp);
    }
}

(8)主函数及部分测试用例

int main() {
    Node* head = NULL;
    insert(&head, 1, 0); // 插入到头部
    insert(&head, 2, 1); // 插入到尾部
    insert(&head, 3, 1); // 插入到中间
    printList(head); // 输出:1 -> 3 -> 2 -> NULL

    deleteNode(&head, 1); // 删除中间节点
    printList(head); // 输出:1 -> 2 -> NULL

    Node* found = search(head, 2);
    if (found) {
        printf("找到节点:%d\n", found->data); // 输出:找到节点:2
    }
    else {
        printf("节点未找到\n");
    }

    freeList(head);
    return 0;
}

2.双向链表

(1)结构定义

        双向链表在结构定义时比单向链表多了一个指向上一结点的指针,在遍历查找时比单向链表更为迅速。在插入与删除时需要修改更多的指针,此处主要介绍插入与删除时的操作。

// 定义节点结构
typedef struct DNode {
    int data;             // 节点数据
    struct DNode* prev;   // 指向前一个节点的指针
    struct DNode* next;   // 指向下一个节点的指针
} DNode;

(2)创建结点 

DNode* createDNode(int data) {
    DNode* newNode = (DNode*)malloc(sizeof(DNode));
    newNode->data = data;
    newNode->prev = NULL;
    newNode->next = NULL;
    return newNode;
}

(3)插入新结点

        如果插入链表头部则将新结点的next指向目前的头结点,当目前的头结点不为空即链表中已经存在结点时,将目前的头结点的prev指向新结点,最后将插入的新结点设为新的头结点。

        否则从头结点开始向后遍历链表(也可以提前记录尾结点从后向前遍历,有多种方案),遍历到插入位置的上一个结点,如果插入位置合法,则将新结点插入到目标位置。插入过程与单向链表基本一致,需要修改prev指针即可。

void insert(DNode** head, int data, int position) {
    DNode* newNode = createDNode(data);
    if (position == 0) {
        // 插入头部
        newNode->next = *head;
        if (*head != NULL) {
            (*head)->prev = newNode;
        }
        *head = newNode;
    }
    else {
        DNode* temp = *head;
        for (int i = 0; temp != NULL && i < position - 1; i++) {
            temp = temp->next;
        }
        if (temp == NULL) {
            printf("插入位置无效\n");
            free(newNode);
        }
        else {
            // 插入中间或尾部
            newNode->next = temp->next;
            if (temp->next != NULL) {
                temp->next->prev = newNode;
            }
            temp->next = newNode;
            newNode->prev = temp;
        }
    }
}

(4)删除结点 

        如果删除的是头结点,只需要将头结点的下一个结点设为新的头结点,赋值完成后若当前结点不为空,则将当前结点的prev指针指向空,释放原来的头结点内存。

        否则与单向链表一致,只需要多修改一次prev指针即可。

void deleteNode(DNode** head, int position) {
    if (*head == NULL) {
        printf("链表为空\n");
        return;
    }

    DNode* temp = *head;

    if (position == 0) {
        // 删除头部节点
        *head = temp->next;
        if (*head != NULL) {
            (*head)->prev = NULL;
        }
        free(temp);
    }
    else {
        for (int i = 0; temp != NULL && i < position; i++) {
            temp = temp->next;
        }
        if (temp == NULL) {
            printf("删除位置无效\n");
        }
        else {
            // 删除中间或尾部节点
            if (temp->prev != NULL) {
                temp->prev->next = temp->next;
            }
            if (temp->next != NULL) {
                temp->next->prev = temp->prev;
            }
            free(temp);
        }
    }
}

3.循环链表(单向)

        循环链表的结构定义与普通链表相同,此处只介绍单向循环链表的插入与删除操作。

(1)插入新结点

        插入原理与单向链表一致。如果插入链表头部,且当前头指针为空(插入第一个元素),则将该结点的next指向自己,即自成环。否则遍历链表找到尾结点,将尾结点的next指向新结点,新结点的next指向当前头结点,再设插入的新结点为新的头结点。

        如果插入其他位置,与单向链表一致。

void insert(Node** head, int data, int position) {
    Node* newNode = createNode(data);
    if (position == 0) {
        if (*head == NULL) {
            newNode->next = newNode; // 自成环
            *head = newNode;
        }
        else {
            Node* temp = *head;
            while (temp->next != *head) {
                temp = temp->next;
            }
            temp->next = newNode;
            newNode->next = *head;
            *head = newNode;
        }
    }
    else {
        Node* temp = *head;
        for (int i = 0; temp->next != *head && i < position - 1; i++) {
            temp = temp->next;
        }
        if (temp->next == *head) {
            printf("插入位置无效\n");
            free(newNode);
        }
        else {
            newNode->next = temp->next;
            temp->next = newNode;
        }
    }
}

(2)删除结点

        如果删除的是头结点,当头结点自成环时直接释放头结点内存即可。否则遍历到尾结点,将尾结点的next指向头结点的下一个结点,将头结点的下一个结点设为新的头结点。

        删除其他位置的结点与单向链表一致。

void deleteNode(Node** head, int position) {
    if (*head == NULL) {
        printf("链表为空\n");
        return;
    }

    Node* temp = *head;

    if (position == 0) {
        // 删除头部节点
        if ((*head)->next == *head) {
            free(*head);
            *head = NULL;
        }
        else {
            Node* last = *head;
            while (last->next != *head) {
                last = last->next;
            }
            last->next = (*head)->next;
            free(*head);
            *head = last->next;
        }
    }
    else {
        Node* prev = NULL;
        for (int i = 0; temp->next != *head && i < position; i++) {
            prev = temp;
            temp = temp->next;
        }
        if (temp->next == *head) {
            printf("删除位置无效\n");
        }
        else {
            prev->next = temp->next;
            free(temp);
        }
    }
}

全文完,赏个点赞收藏吧~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值