数据结构与C++

数据结构与C++ 知识量:9 - 32 - 91

2.3 线性表的链式存储结构><

单链表结构- 2.3.1 -

链表是一种线性表的存储结构,它通过一组任意的存储单元来存储数据元素。为了表示数据元素之间的线性关系,链表采用了一种特殊的数据结构,称为结点。每个结点包含两个部分:数据域和指针域。

数据域用于存储数据元素的实际信息,而指针域则存储其后继结点的地址信息。通过每个结点的指针域,可以建立起数据元素之间的线性关系,形成所谓的“链”。这个链就是链表的名称由来。

由于每个结点中只有一个指向后继的指针,所以这种链表被称为单链表。在单链表中,每个结点只有一个指向下一个结点的链接,因此只能按照一个方向遍历链表。

为了操作链表,通常需要定义几个基本操作,如初始化链表、插入结点、删除结点、查找结点等。这些操作可以在链表的头部、尾部或指定位置执行,以实现添加、删除和查找等操作。

总的来说,链表是一种灵活的线性表存储结构,它通过结点和指针域来表示数据元素之间的线性关系。单链表是其中一种常见的链表形式,具有简单和高效的特点。

单链表运算- 2.3.2 -

单链表的运算主要包括以下几种:

  • 初始化单链表:创建一个空的单链表。

  • 插入元素:在链表的头部、尾部或指定位置插入一个元素。

  • 删除元素:删除链表头部、尾部或指定位置的元素。

  • 查找元素:查找链表中是否存在指定元素,如果存在则返回其位置,否则返回-1。

  • 修改元素:修改指定位置的元素值。

  • 判断是否为空:判断单链表是否为空。

  • 获取长度:返回链表的长度(元素个数)。

  • 清空:删除链表中的所有元素。

  • 遍历:按顺序访问链表中的每个元素。

以下是一个简单的示例,展示了如何使用C++实现单链表的运算:

#include <iostream>  
  
struct Node {  
    int data;  
    Node* next;  
};  
  
class LinkedList {  
private:  
    Node* head;  
public:  
    LinkedList() {  
        head = nullptr;  
    }  
  
    // 插入元素到链表头部  
    void insertAtHead(int data) {  
        Node* newNode = new Node();  
        newNode->data = data;  
        newNode->next = head;  
        head = newNode;  
    }  
  
    // 插入元素到链表尾部  
    void insertAtTail(int data) {  
        Node* newNode = new Node();  
        newNode->data = data;  
        newNode->next = nullptr;  
        if (head == nullptr) {  
            head = newNode;  
        } else {  
            Node* current = head;  
            while (current->next != nullptr) {  
                current = current->next;  
            }  
            current->next = newNode;  
        }  
    }  
  
    // 删除指定位置的元素  
    void deleteElement(int index) {  
        if (head == nullptr) {  
            return;  
        }  
        if (index == 0) {  
            Node* temp = head;  
            head = head->next;  
            delete temp;  
            return;  
        } else {  
            Node* current = head;  
            int count = 0;  
            while (current != nullptr && count < index - 1) {  
                current = current->next;  
                count++;  
            }  
            if (current != nullptr) {  
                Node* temp = current->next;  
                current->next = temp->next;  
                delete temp;  
            } else {  
                std::cout << "Index out of bounds";  
            }  
        }  
    }  
  
    // 查找元素是否存在,并返回位置或-1(不存在)  
    int search(int data) {  
        Node* current = head;  
        int index = 0;  
        while (current != nullptr) {  
            if (current->data == data) {  
                return index;  
            } else {  
                current = current->next;  
                index++;  
            }  
        }  
        return -1; // 不存在该元素,返回-1表示未找到。  
    }  
};

循环链表结构- 2.3.3 -

循环链表(Circular Linked List)是一种链表的变种,其中最后一个结点的指针域指向链表的头结点,从而形成一个闭环。与常规的单链表相比,循环链表在结构上更加灵活,并具有一些独特的性质和用途。

在循环链表中,每个结点包含数据域和指针域,与单链表类似。数据域用于存储实际的数据元素,而指针域则用于指向下一个结点。然而,在循环链表中,最后一个结点的指针域不再指向空,而是指向链表的头结点。

这种结构使得循环链表具有以下几个特点:

  • 循环性:由于最后一个结点的指针域指向头结点,所以可以从链表的任意位置开始遍历,并一直循环下去,直到再次回到起始位置。

  • 无边界:循环链表没有明确的起点和终点,每个结点都可以被视为起点或终点。这种无边界的特性使得循环链表在某些应用中更加灵活和方便。

  • 双向遍历:虽然循环链表本身并不提供双向遍历的功能,但可以通过在每个结点中增加一个指向前一个结点的指针来实现双向循环链表。这样,就可以在两个方向上遍历链表。

循环链表的应用包括:

  • 循环队列:循环链表可以用作循环队列的实现。在循环队列中,头尾相连形成一个环,可以通过移动头尾指针来实现队列的入队和出队操作。

  • 约瑟夫问题:约瑟夫问题是一个经典的数学问题,可以使用循环链表来解决。问题描述为:n个人围成一圈,从第一个人开始报数,每数到m的人出列,直到所有人都出列为止。通过构建循环链表并模拟报数过程,可以找到每次出列的人的序号。

  • 环形缓冲区:循环链表也可以用于实现环形缓冲区(Circular Buffer),这是一种先进先出的数据结构,常用于存储和管理数据流。

需要注意的是,在处理循环链表时,要特别注意避免无限循环和访问已经释放的内存。在遍历循环链表时,应该使用计数器或设置终止条件来确保遍历的正确终止。

以下是一个使用C++实现循环链表的简单示例:

#include <iostream>  
  
struct Node {  
    int data;  
    Node* next;  
};  
  
class CircularLinkedList {  
private:  
    Node* head;  
public:  
    CircularLinkedList() {  
        head = nullptr;  
    }  
  
    // 插入元素到链表尾部  
    void insert(int data) {  
        Node* newNode = new Node();  
        newNode->data = data;  
        newNode->next = nullptr;  
  
        if (head == nullptr) {  
            head = newNode;  
            head->next = head; // 将头结点的指针指向自身,形成循环  
        } else {  
            Node* current = head;  
            while (current->next != head) {  
                current = current->next;  
            }  
            current->next = newNode; // 将新结点插入到尾部  
            newNode->next = head; // 新结点的指针指向头结点,形成循环  
        }  
    }  
  
    // 遍历循环链表并打印元素  
    void display() {  
        if (head == nullptr) {  
            std::cout << "链表为空" << std::endl;  
            return;  
        }  
  
        Node* current = head;  
        do {  
            std::cout << current->data << " ";  
            current = current->next;  
        } while (current != head); // 遍历直到回到头结点  
        std::cout << std::endl;  
    }  
};  
  
int main() {  
    CircularLinkedList list;  
    list.insert(1);  
    list.insert(2);  
    list.insert(3);  
    list.insert(4);  
    list.insert(5);  
    list.display(); // 输出:1 2 3 4 5   
    return 0;  
}

在这个示例中,定义了一个CircularLinkedList类来实现循环链表。insert函数用于在链表的尾部插入新元素,display函数用于遍历循环链表并打印元素。在main函数中,创建了一个循环链表对象,并插入了一些元素,然后调用display函数来打印链表中的元素。

双向链表结构- 2.3.4 -

双向链表(Doubly Linked List)是一种更复杂的链表结构,它在单链表的基础上进行了扩展。在双向链表中,每个结点包含三个域:数据域、前驱指针域和后继指针域。数据域用于存储实际的数据元素,前驱指针域指向前一个结点,后继指针域指向后一个结点。

与单链表相比,双向链表具有以下特点:

  • 双向遍历:由于每个结点都包含指向前驱和后继的指针,因此可以从任意结点开始,在两个方向上遍历链表。这使得双向链表在遍历操作方面更加灵活。

  • 插入和删除操作:在双向链表中,插入和删除结点的操作比单链表稍微复杂一些。当插入一个结点时,需要同时更新其前驱和后继结点的指针;当删除一个结点时,需要同时处理其前驱和后继结点的指针。然而,由于可以直接访问前驱和后继结点,这些操作的平均时间复杂度仍然为O(1)。

  • 空间开销:双向链表需要为每个结点分配额外的空间来存储前驱指针。因此,与单链表相比,双向链表的空间开销更大。

双向链表的应用场景包括:

  • 需要频繁进行插入和删除操作的场景:双向链表的插入和删除操作相对简单且高效,因此在需要频繁进行这些操作的场景中,使用双向链表可以提高性能。

  • 需要双向遍历的场景:如果需要在链表中进行双向遍历,例如在某些算法或数据结构中,双向链表可以提供方便的遍历方式。

需要注意的是,在处理双向链表时,要特别注意避免出现野指针(悬挂指针)的情况。在删除结点或重新分配内存时,务必确保正确更新相关结点的指针,以避免指向无效内存区域的风险。

以下是一个使用C++实现双向链表的简单示例:

#include <iostream>  
  
struct Node {  
    int data;  
    Node* prev;  
    Node* next;  
};  
  
class DoublyLinkedList {  
private:  
    Node* head;  
public:  
    DoublyLinkedList() {  
        head = nullptr;  
    }  
  
    // 插入元素到链表尾部  
    void insert(int data) {  
        Node* newNode = new Node();  
        newNode->data = data;  
        newNode->prev = nullptr;  
        newNode->next = nullptr;  
  
        if (head == nullptr) {  
            head = newNode;  
        } else {  
            Node* current = head;  
            while (current->next != nullptr) {  
                current = current->next;  
            }  
            current->next = newNode;  
            newNode->prev = current;  
        }  
    }  
  
    // 遍历双向链表并打印元素  
    void display() {  
        if (head == nullptr) {  
            std::cout << "链表为空" << std::endl;  
            return;  
        }  
  
        Node* current = head;  
        while (current != nullptr) {  
            std::cout << current->data << " ";  
            current = current->next;  
        }  
        std::cout << std::endl;  
    }  
};  
  
int main() {  
    DoublyLinkedList list;  
    list.insert(1);  
    list.insert(2);  
    list.insert(3);  
    list.insert(4);  
    list.insert(5);  
    list.display(); // 输出:1 2 3 4 5   
    return 0;  
}

在这个示例中,定义了一个DoublyLinkedList类来实现双向链表。Node结构体包含三个成员变量:data用于存储数据,prev指向前一个结点,next指向后一个结点。insert函数用于在链表的尾部插入新元素,display函数用于遍历双向链表并打印元素。在main函数中,创建了一个双向链表对象,并插入了一些元素,然后调用display函数来打印链表中的元素。这个示例展示了双向链表的基本操作和遍历方式。

顺序表与链式表的比较- 2.3.5 -

顺序表和链式表是两种常用的数据结构,它们在存储和操作方式上有显著的差异。以下是它们的具体比较:

1. 存储方式:

  • 顺序表:采用静态分配方式,其存储密度为1。它不需要为表示结点间的逻辑关系而增加额外的存储开销。

  • 链式表:使用动态分配方式,因此其存储密度小于1。

2. 存取方式:

  • 顺序表:支持随机存取,即可以直接访问任意位置的元素。

  • 链式表:仅支持顺序存取,即从头节点开始按顺序访问。

3. 插入和删除操作:

  • 顺序表:进行插入或删除时,平均需要移动表中一半的元素,当数据元素的信息量较大且表较长时,这一点是不应忽视的。

  • 链式表:插入和删除操作影响的只有一个节点,不需要移动其他元素,但在进行插入或删除操作时需要找到插入位置或删除节点。

4. 空间性能:

  • 顺序表:需要预先分配一块连续的内存空间,可能会造成一定的空间浪费。

  • 链式表:不需要连续的内存空间,可以充分利用内存资源,但需要额外的空间来存储指针信息。

5. 时间性能:

  • 顺序表:在查找操作上,如果数据已按序排列,可以使用二分查找法提高效率。在插入和删除操作上,时间复杂度为O(N)。

  • 链式表:查找操作需要从头节点开始遍历,时间复杂度较高。但插入和删除操作的时间复杂度为O(1),优于顺序表。

总的来说,顺序表和链式表各有优缺点,选择哪种数据结构取决于具体的应用场景和需求。例如,如果经常需要按序号访问数据元素,顺序表会更优;而如果需要频繁进行插入和删除操作,链式表则更为合适。