为了账号安全,请及时绑定邮箱和手机立即绑定

链表学习:从入门到实践的简单教程

概述

本文详细介绍了链表学习中的基本概念、类型及操作,包括单链表、双向链表和循环链表的创建与使用。文章还探讨了链表在插入、删除和遍历等基本操作中的优势,并提供了多种链表相关的编程挑战和实际应用案例。通过这些内容,读者可以全面了解链表的特点和应用场景,进一步掌握链表学习。

链表的基本概念

链表是一种线性数据结构,它不像数组那样通过索引直接访问数据,而是通过一组节点来存储数据。每个节点包含两部分:数据域(存储数据)和指针域(指向下一个节点的地址)。链表的这种结构使得它在插入和删除操作上比数组更有效率。

什么是链表

链表是一种数据结构,它通过指针将一系列节点串联起来。每个节点包含两部分:数据域(用于存储数据)和指针域(用于存储指向下一个节点的指针)。链表中的节点顺序通过指针域的链接来确定。

链表与数组的区别

  • 存储方式:数组使用连续的内存空间存储数据,而链表使用非连续的内存空间存储数据。
  • 访问时间:数组可以通过索引直接访问任意一个元素,时间复杂度为O(1);链表则需要从头节点开始逐个遍历,时间复杂度为O(n)。
  • 存储空间:数组的大小在创建时固定,难以增删;链表大小可以动态调整。
  • 内存分配:数组在创建时需要预先分配足够的内存;链表则不需要预先分配,可以在需要时创建新的节点。

链表的优势和应用场景

  • 动态增删:链表可以随时插入或删除节点,不需要重新分配内存。
  • 顺序存储:链表可以顺序存储数据,不需要连续的内存空间。
  • 适合频繁插入删除:链表在插入和删除操作上比数组更高效,时间复杂度为O(1),而数组需要移动元素。
  • 应用场景:链表常用于实现队列、栈等数据结构,以及在内存管理中的内存分配和回收。
单链表的创建

单链表是最基本的链表类型,每个节点包含一个数据域和一个指向下一个节点的指针域。单链表的创建包括定义节点结构、初始化头指针和初始化链表。

单链表的节点结构

每个节点结构通常包含一个数据域和一个指向下一个节点的指针域。例如,定义一个单链表节点可以如下:

struct Node {
    int data;       // 数据域
    Node* next;     // 指针域,指向下一个节点
};

单链表的头指针

单链表通过一个头指针来访问整个链表。头指针指向链表的第一个节点。如果链表为空,头指针指向NULL。

单链表的初始化

单链表的初始化通常包括创建头指针和初始化头指针指向NULL。例如:

Node* head = nullptr;  // 初始化头指针为NULL
单链表的基本操作

单链表的基本操作包括插入节点、删除节点和遍历输出节点。

在链表中插入节点

在链表中插入节点可以通过以下步骤完成:

  1. 创建一个新的节点。
  2. 更新新节点的指针域,使其指向当前节点的下一个节点。
  3. 更新当前节点的指针域,使其指向新节点。

例如,假设要在一个单链表中插入一个新的节点:

void insert(Node*& head, int value, int position) {
    Node* newNode = new Node;
    newNode->data = value;
    newNode->next = nullptr;
    if (head == nullptr || position == 0) {
        newNode->next = head;
        head = newNode;
    } else {
        Node* current = head;
        for (int i = 0; i < position - 1 && current != nullptr; i++) {
            current = current->next;
        }
        if (current != nullptr) {
            newNode->next = current->next;
            current->next = newNode;
        }
    }
}

在链表中删除节点

在链表中删除节点可以通过以下步骤完成:

  1. 找到要删除节点的前一个节点。
  2. 更新前一个节点的指针域,使其指向被删除节点的下一个节点。
  3. 释放被删除节点的内存。

例如,假设要在一个单链表中删除一个节点:

void remove(Node*& head, int value) {
    Node* current = head;
    Node* previous = nullptr;
    while (current != nullptr && current->data != value) {
        previous = current;
        current = current->next;
    }
    if (current != nullptr) {
        if (previous != nullptr) {
            previous->next = current->next;
        } else {
            head = current->next;
        }
        delete current;
    }
}

遍历链表并输出节点

遍历链表并输出节点可以通过以下步骤完成:

  1. 从头指针开始遍历链表。
  2. 访问每个节点的数据域。
  3. 输出数据域的值。

例如,假设要遍历一个单链表并输出节点的值:

void printList(Node* head) {
    Node* current = head;
    while (current != nullptr) {
        std::cout << current->data << " ";
        current = current->next;
    }
    std::cout << std::endl;
}
双向链表的引入

双向链表是一种比单链表更复杂的数据结构,每个节点包含两个指针域,一个指向下一个节点,另一个指向前一个节点。双向链表的特点和基本操作包括插入节点、删除节点和遍历输出节点。

双向链表的节点结构

每个节点结构通常包含一个数据域和两个指针域,一个指向下一个节点,另一个指向前一个节点。例如,定义一个双向链表节点可以如下:

struct Node {
    int data;          // 数据域
    Node* next;        // 指向下一个节点的指针
    Node* prev;        // 指向前一个节点的指针
};

双向链表的特点

  • 双向访问:双向链表可以通过前向指针和后向指针双向访问数据。
  • 方便删除:双向链表在删除节点时不需要额外的指针来查找前一个节点。
  • 内存开销:双向链表需要比单链表更多的内存,因为每个节点需要存储两个指针。

双向链表的基本操作

双向链表的基本操作包括插入节点、删除节点和遍历输出节点。

在链表中插入节点

在双向链表中插入节点可以通过以下步骤完成:

  1. 创建一个新的节点。
  2. 更新新节点的指针域,使其指向当前节点的下一个节点。
  3. 更新当前节点的指针域,使其指向新节点。
  4. 更新新节点的指针域,使其指向前一个节点。

例如,假设要在一个双向链表中插入一个新的节点:

void insert(Node*& head, int value, int position) {
    Node* newNode = new Node;
    newNode->data = value;
    newNode->next = nullptr;
    newNode->prev = nullptr;
    if (head == nullptr || position == 0) {
        newNode->next = head;
        if (head != nullptr) {
            head->prev = newNode;
        }
        head = newNode;
    } else {
        Node* current = head;
        for (int i = 0; i < position - 1 && current != nullptr; i++) {
            current = current->next;
        }
        if (current != nullptr) {
            newNode->next = current->next;
            if (current->next != nullptr) {
                current->next->prev = newNode;
            }
            newNode->prev = current;
            current->next = newNode;
        }
    }
}

在链表中删除节点

在双向链表中删除节点可以通过以下步骤完成:

  1. 找到要删除节点的前一个节点和后一个节点。
  2. 更新前一个节点的指针域,使其指向被删除节点的下一个节点。
  3. 更新后一个节点的指针域,使其指向前一个节点。
  4. 释放被删除节点的内存。

例如,假设要在一个双向链表中删除一个节点:

void remove(Node*& head, int value) {
    Node* current = head;
    while (current != nullptr && current->data != value) {
        current = current->next;
    }
    if (current != nullptr) {
        if (current->prev != nullptr) {
            current->prev->next = current->next;
        } else {
            head = current->next;
        }
        if (current->next != nullptr) {
            current->next->prev = current->prev;
        }
        delete current;
    }
}

遍历链表并输出节点

遍历双向链表并输出节点可以通过以下步骤完成:

  1. 从头指针开始遍历链表。
  2. 访问每个节点的数据域。
  3. 输出数据域的值。

例如,假设要遍历一个双向链表并输出节点的值:

void printList(Node* head) {
    Node* current = head;
    while (current != nullptr) {
        std::cout << current->data << " ";
        current = current->next;
    }
    std::cout << std::endl;
}
循环链表的介绍

循环链表是一种特殊的链表,其最后一个节点的指针域指向头节点,形成一个循环。循环链表的特点和基本操作包括插入节点、删除节点和遍历输出节点。

循环链表的定义

循环链表是一种链表,其最后一个节点的指针域指向头节点,形成一个循环。循环链表可以通过单链表或双向链表实现。

循环链表的特点

  • 循环结构:循环链表的最后一个节点的指针域指向头节点,形成一个循环。
  • 遍历方便:循环链表遍历方便,无需判断是否到达链表末尾。
  • 内存开销:循环链表的实现需要在单链表或双向链表的基础上增加额外的指针操作。

循环链表的实现

循环链表可以在单链表或双向链表的基础上实现。以下是单链表实现循环链表的基本操作示例:

在链表中插入节点

在循环链表中插入节点可以通过以下步骤完成:

  1. 创建一个新的节点。
  2. 更新新节点的指针域,使其指向头节点。
  3. 更新当前节点的指针域,使其指向新节点。

例如,假设要在一个循环链表中插入一个新的节点:

void insert(Node*& head, int value) {
    Node* newNode = new Node;
    newNode->data = value;
    newNode->next = nullptr;
    if (head == nullptr) {
        head = newNode;
        newNode->next = head;
    } else {
        Node* current = head;
        while (current->next != head) {
            current = current->next;
        }
        newNode->next = head;
        current->next = newNode;
        head = newNode;
    }
}

在链表中删除节点

在循环链表中删除节点可以通过以下步骤完成:

  1. 找到要删除节点的前一个节点。
  2. 更新前一个节点的指针域,使其指向被删除节点的下一个节点。
  3. 释放被删除节点的内存。

例如,假设要在一个循环链表中删除一个节点:

void remove(Node*& head, int value) {
    if (head == nullptr) {
        return;
    }
    if (head->data == value && head->next == head) {
        delete head;
        head = nullptr;
        return;
    }
    Node* current = head;
    while (current->next != head && current->next->data != value) {
        current = current->next;
    }
    if (current->next != head && current->next->data == value) {
        Node* toDelete = current->next;
        current->next = toDelete->next;
        if (toDelete->next == head) {
            head = current->next;
        }
        delete toDelete;
    }
}

遍历链表并输出节点

遍历循环链表并输出节点可以通过以下步骤完成:

  1. 从头指针开始遍历链表。
  2. 访问每个节点的数据域。
  3. 输出数据域的值。

例如,假设要遍历一个循环链表并输出节点的值:

void printList(Node* head) {
    if (head == nullptr) {
        return;
    }
    Node* current = head;
    do {
        std::cout << current->data << " ";
        current = current->next;
    } while (current != head);
    std::cout << std::endl;
}
实战练习

经典链表问题练习

问题1:反转链表

反转一个单链表是指将链表中的元素顺序反转。例如,给定链表 1 -> 2 -> 3 -> 4 -> 5,反转后的链表应为 5 -> 4 -> 3 -> 2 -> 1

Node* reverseList(Node* head) {
    Node* prev = nullptr;
    Node* current = head;
    while (current != nullptr) {
        Node* next = current->next;
        current->next = prev;
        prev = current;
        current = next;
    }
    return prev;
}

问题2:合并两个有序链表

合并两个有序链表是指将两个有序链表合并为一个有序链表。例如,给定链表 1 -> 3 -> 52 -> 4 -> 6,合并后的链表应为 1 -> 2 -> 3 -> 4 -> 5 -> 6

Node* mergeList(Node* head1, Node* head2) {
    Node dummy;
    Node* current = &dummy;
    while (head1 != nullptr && head2 != nullptr) {
        if (head1->data <= head2->data) {
            current->next = head1;
            head1 = head1->next;
        } else {
            current->next = head2;
            head2 = head2->next;
        }
        current = current->next;
    }
    current->next = (head1 != nullptr) ? head1 : head2;
    return dummy.next;
}

问题3:判断链表是否为循环链表

判断一个链表是否为循环链表是指判断链表中是否存在一个节点,该节点的指针域指向其前一个节点。例如,给定链表 1 -> 2 -> 3 -> 4 -> 5,如果最后的 5 指向 3,则该链表为循环链表。

bool isCircularList(Node* head) {
    if (head == nullptr) {
        return false;
    }
    Node* slow = head;
    Node* fast = head;
    while (fast != nullptr && fast->next != nullptr) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) {
            return true;
        }
    }
    return false;
}

链表相关的编程挑战

挑战1:删除链表中的重复元素

删除链表中的重复元素是指删除链表中所有值重复的节点,只保留唯一的节点。例如,给定链表 1 -> 1 -> 2 -> 2 -> 3,删除重复元素后的链表应为 3

Node* removeDuplicates(Node* head) {
    if (head == nullptr) {
        return nullptr;
    }
    Node* current = head;
    while (current != nullptr && current->next != nullptr) {
        if (current->data == current->next->data) {
            Node* toDelete = current->next;
            current->next = toDelete->next;
            delete toDelete;
        } else {
            current = current->next;
        }
    }
    return head;
}

挑战2:找到链表的中间节点

找到链表的中间节点是指找到链表中的中间节点。例如,给定链表 1 -> 2 -> 3 -> 4 -> 5,中间节点为 3;给定链表 1 -> 2 -> 3 -> 4,中间节点为 2.5(实际链表节点为 23)。

Node* findMiddleNode(Node* head) {
    if (head == nullptr) {
        return nullptr;
    }
    Node* slow = head;
    Node* fast = head;
    while (fast != nullptr && fast->next != nullptr) {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

挑战3:求两个链表的交点

求两个链表的交点是指找到两个链表的第一个共同节点。例如,给定链表 A = 1 -> 2 -> 3 -> 4 和链表 B = 5 -> 6 -> 3 -> 4,交点为 3

Node* findIntersection(Node* head1, Node* head2) {
    Node* current1 = head1;
    Node* current2 = head2;
    while (current1 != current2) {
        current1 = (current1 != nullptr) ? current1->next : head2;
        current2 = (current2 != nullptr) ? current2->next : head1;
    }
    return current1;
}

实际项目中的链表应用

应用1:内存管理

在内存管理中,链表可以用来实现内存池,动态分配和回收内存。例如,可以使用单链表来实现一个简单的内存池,其中每个节点表示一个内存块。

struct MemoryBlock {
    void* data;  // 存储数据的指针
    MemoryBlock* next;  // 指向下一个节点的指针
};

MemoryBlock* allocateMemory(MemoryBlock*& head, size_t size) {
    if (head == nullptr) {
        return nullptr;
    }
    MemoryBlock* block = head;
    void* data = malloc(size);
    head = head->next;
    block->data = data;
    block->next = nullptr;
    return block;
}

void freeMemory(MemoryBlock*& head, MemoryBlock* block) {
    block->next = head;
    head = block;
}

应用2:缓存机制

在缓存机制中,链表可以用来实现LRU(最近最少使用)缓存。例如,可以使用双向链表来实现一个简单的LRU缓存,其中头节点表示最常用的数据,尾节点表示最不常用的数据。

struct CacheNode {
    int key;  // 缓存键
    int value;  // 缓存值
    CacheNode* next;  // 指向下一个节点的指针
    CacheNode* prev;  // 指向前一个节点的指针
};

class LRUCache {
private:
    unordered_map<int, CacheNode*> cache;  // 缓存映射
    CacheNode* head;  // 头节点
    CacheNode* tail;  // 尾节点
    int capacity;  // 缓存容量
public:
    LRUCache(int capacity) {
        this->capacity = capacity;
        head = new CacheNode;
        tail = new CacheNode;
        head->next = tail;
        tail->prev = head;
    }

    void put(int key, int value) {
        if (cache.find(key) != cache.end()) {
            remove(cache[key]);
        } else if (cache.size() >= capacity) {
            remove(tail->prev);
        }
        insertHead(key, value);
    }

    int get(int key) {
        if (cache.find(key) == cache.end()) {
            return -1;
        }
        CacheNode* node = cache[key];
        remove(node);
        insertHead(key, node->value);
        return node->value;
    }

private:
    void remove(CacheNode* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
        cache.erase(node->key);
        delete node;
    }

    void insertHead(int key, int value) {
        CacheNode* node = new CacheNode;
        node->key = key;
        node->value = value;
        cache[key] = node;
        node->prev = head;
        node->next = head->next;
        node->next->prev = node;
        head->next = node;
    }
};

应用3:数据结构实现

在数据结构实现中,链表可以用来实现各种高级数据结构。例如,可以使用单链表来实现栈和队列。

class Stack {
private:
    Node* head;  // 头节点
public:
    Stack() {
        head = nullptr;
    }

    void push(int value) {
        Node* newNode = new Node;
        newNode->data = value;
        newNode->next = head;
        head = newNode;
    }

    void pop() {
        if (head == nullptr) {
            return;
        }
        Node* toDelete = head;
        head = head->next;
        delete toDelete;
    }

    int top() {
        if (head == nullptr) {
            return -1;
        }
        return head->data;
    }

    bool isEmpty() {
        return head == nullptr;
    }
};

class Queue {
private:
    Node* head;  // 头节点
    Node* tail;  // 尾节点
public:
    Queue() {
        head = nullptr;
        tail = nullptr;
    }

    void enqueue(int value) {
        Node* newNode = new Node;
        newNode->data = value;
        newNode->next = nullptr;
        if (head == nullptr) {
            head = newNode;
            tail = newNode;
        } else {
            tail->next = newNode;
            tail = newNode;
        }
    }

    void dequeue() {
        if (head == nullptr) {
            return;
        }
        Node* toDelete = head;
        head = head->next;
        if (head == nullptr) {
            tail = nullptr;
        }
        delete toDelete;
    }

    int front() {
        if (head == nullptr) {
            return -1;
        }
        return head->data;
    }

    bool isEmpty() {
        return head == nullptr;
    }
};

通过这些实战练习和应用案例,您可以更好地理解和掌握链表在实际编程中的应用。链表作为一种灵活高效的数据结构,在各种编程场景中都有着广泛的应用。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消