【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)

  • 一、线性表
    • 1.1 概念与特点
    • 1.2 线性表的存储结构
    • 1.3 常见操作
    • 1.4 应用场景
  • 二、链表
    • 2.1 链表简介
    • 2.2 单向链表(单链表)
      • 2.21 基本概念
      • 2.22 单链表基本操作
      • 2.23 C语言实现
        • ▶ 带头结点
        • ▶ 不带头结点
    • 2.3 双向链表
      • 2.31 基本概念
      • 2.32 与单链表比较
      • 2.33 双向链表的应用
      • 2.34基本操作
      • 2.35 C语言实现
        • ▶带头结点的双向链表
        • ▶不带头结点的双向链表
    • 2.4 循环链表
      • 2.41 基本概念
      • 2.42 循环链表的基本操作
      • 2.43 循环链表应用场景
      • 2.44 C语言实现
        • ▶ 带头结点的单向循环链表
        • ▶ 不带头结点的单向循环链表
        • ▶ 带头结点的双向循环链表
        • ▶ 不带头结点的双向循环链表
  • 三、一些高阶操作
    • 3.1 链表反转
    • 3.11 单链表反转
    • 3.12 双向链表反转
    • 3.2 链表排序
    • 3.3 约瑟夫环问题
      • 3.31 问题重述
      • 3.32 链表解决
    • 3.4 块状链表
      • 3.41 概念
      • 3.42 C语言实现

一、线性表

线性表是一种最基本、最简单的数据结构,数据元素之间仅具有单一的前驱和后继关系,它提供了简单而强大的方法来处理和操作数据集合。

1.1 概念与特点

  1. 定义:线性表是由n(n≥0)个数据元素组成的有限序列,其中每个元素只有一个前驱和一个后继(除了第一个和最后一个元素)。
  2. 特点:
    • 线性结构:线性表中的元素之间存在线性关系,每个元素仅有一个直接前驱和一个直接后继。
    • 有序性:线性表中的元素按照其在序列中的位置依次排列,位置由线性表的首尾确定。
    • 可变性:线性表的长度可以动态地增加或减少。

逻辑结构

线性表的数据元素具有抽象(即不确定)的数据类型,在实际问题中,数据元素的抽象类
型将被具体的数据类型所取代。(常见整形和结构体)

1.2 线性表的存储结构

线性表的存储结构有两种常见的实现方式:顺序存储和链式存储。

(1)顺序存储结构:

顺序存储结构使用一段连续的内存空间来存储线性表中的元素。在内存中,线性表的元素按照其在序列中的位置依次存放。其中,首元素存放在起始地址,后续元素紧接着存放在连续的内存单元中。

  • 优点:
    • 随机访问:由于元素在内存中的连续存储,可以通过元素的下标快速直接访问元素。
    • 简单高效:存取元素的时间复杂度为O(1),适用于频繁访问和随机访问元素的场景。
  • 缺点:
    • 大小固定:顺序存储结构需要预先分配一段连续的内存空间,导致大小固定,不易扩展。
    • 插入和删除操作效率低:在插入和删除元素时,需要移动其他元素的位置,导致效率较低。

(2)链式存储结构:

链式存储结构通过节点之间的指针连接来存储线性表中的元素。每个节点包含两部分信息:数据域用于存储元素值,指针域用于指向下一个节点的地址。

  • 优点:

    • 灵活扩展:链式存储结构可以动态地分配内存,便于根据实际需求进行大小的调整。
    • 插入和删除操作效率高:在插入和删除元素时,只需调整相邻节点的指针,效率较高。
  • 缺点:

    • 随机访问困难:由于元素之间不是连续存储的,无法通过下标直接访问元素,需要从头节点开始依次遍历至目标位置。
    • 需要额外的指针空间:链式存储结构需要额外的指针空间来存储节点之间的连接关系。

总结:

顺序存储结构适用于频繁访问和随机访问元素的场景,而链式存储结构适用于动态大小和频繁插入、删除操作的场景

1.3 常见操作

  1. 插入操作:向线性表的指定位置插入一个新元素,需要移动后续元素的位置。
  2. 删除操作:从线性表中删除指定位置的元素,需要移动后续元素的位置。
  3. 查找操作:根据元素的值或位置在线性表中进行查找,可以获取指定元素或元素所在位置的信息。
  4. 修改操作:修改线性表中指定位置的元素的值。
  5. 获取长度:获取线性表中元素的个数。

1.4 应用场景

线性表作为一种基本的数据结构,广泛应用于各个领域,包括但不限于以下几个方面:

  1. 数组:数组是线性表的一种实现方式,用于存储具有相同类型的数据集合,广泛用于数据存储和处理、图像处理、数值计算等领域。
  2. 链表:链表是线性表的另一种实现方式,通过指针将元素连接起来,常用于动态内存管理、数据库系统、操作系统等领域。
  3. :栈是一种特殊的线性表,遵循"先进后出"(LIFO)的原则,常用于函数调用、表达式求值、撤销操作等场景。
  4. 队列:队列是一种特殊的线性表,遵循"先进先出"(FIFO)的原则,常用于任务调度、消息传递、缓冲区管理等场景。

二、链表

2.1 链表简介

链表是一种常见且重要的数据结构,用于组织和存储数据。相比于顺序存储结构,链表通过节点之间的指针连接实现元素的存储和访问。本节将详细介绍链表的概念、特点、常见的类型以及操作和应用场景。

(1)概念与特点:

  1. 定义:链表是由一系列节点组成的数据结构,每个节点包含一个数据元素和一个指向下一个节点的指针。
  2. 特点:
    • 链式结构:链表中的节点通过指针相互连接,形成链式结构。
    • 无序性:链表中的节点可以按任意顺序排列。
    • 可变性:链表的长度可以动态地增加或减少,不需要预先分配固定大小的内存空间。

(2)常见类型:

  1. 单向链表:每个节点只包含一个指向下一个节点的指针,最后一个节点指向空值。
  2. 双向链表:每个节点同时包含指向前一个节点和后一个节点的指针,使得可以在链表中进行双向遍历。
  3. 循环链表:最后一个节点的指针指向头节点,形成一个闭环的链表结构。

(3)常见操作:

  1. 插入操作:在链表的指定位置插入一个新节点,需要调整相邻节点的指针。
  2. 删除操作:从链表中删除指定位置的节点,需要调整相邻节点的指针。
  3. 查找操作:根据节点的值或位置在链表中进行查找,可以获取指定节点或节点所在位置的信息。
  4. 修改操作:修改链表中指定节点的值。
  5. 获取长度:获取链表中节点的个数。

(4)应用场景:

链表作为一种灵活的数据结构,广泛应用于各个领域,特别适用于以下几个方面:

  1. 动态数据集合:由于链表的可变性,适用于需要频繁插入和删除操作的场景,如实时数据流处理、编辑器的撤销操作等。
  2. 内存管理:链表的灵活性使其成为动态内存分配的基础,用于管理堆内存中的空闲块或垃圾回收算法。
  3. 数据结构的实现:链表是许多其他高级数据结构的基础,如栈、队列和图等,提供了灵活性和效率的支持。

2.2 单向链表(单链表)

2.21 基本概念

单链表(singly linked list)是用一组任意的存储单元存放线性表的元素,这组存储单元可以连续也可以不连续,甚至可以零散分布在内存中的任意位置。

为了能正确表示元素之间的逻辑关系,每个存储单元在存储数据元素的同时,还必须存储其后继元素所在的地址信息,即指针,这两部分组成了数据元素的存储映像,称为结点(node)。
在这里插入图片描述

单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起的,由于每个结点只有一个指针域,故称为单链表。

单链表中每个结点的存储地址存放在其前驱结点的 指针域中,而第一个元素无前驱,所以设头指针(head pointer)指向第一个元素所在结点(称为开始结点),整个单链表的存取必须从头指针开始进行,因而头指针具有标识一个单链表的作用;由于最后一个元素无后继,故最后一个元素所在结点(称为终端结点)的指针域为空,即NULL(图中用“∧”表示),也称尾标志(tail mark)。

在这里插入图片描述

上面的表述中:元素从第一个节点开始存储,看起来没什么问题。但是当链表为空、在最前面插入和删除元素、遍历链表时会更加麻烦。所以,通常在第一个元素的节点前面添加一个头结点。(在后面的介绍中,可以想象没有头结点应该如何操作)

【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第1张图片

2.22 单链表基本操作

细介绍单链表的各种操作(大概的操作,没有细分是否带头结点,详见代码部分):

(1)创建链表:

创建一个空的单链表需要初始化一个头指针,并将头指针指向空值。

(2)插入操作:

  • 头部插入:创建一个新节点,将新节点的指针指向原头节点,再将头指针指向新节点。
  • 尾部插入:遍历链表,找到最后一个节点,将最后一个节点的指针指向新节点,再将新节点的指针指向空值。
  • 中间插入:找到插入位置的前一个节点,将新节点的指针指向前一个节点的下一个节点,再将前一个节点的指针指向新节点。
    【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第2张图片
    动图:
    【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第3张图片

(3)删除操作:

  • 删除头节点:将头指针指向头节点的下一个节点,并释放原头节点的内存。
  • 删除尾节点:遍历链表,找到倒数第二个节点,将该节点的指针指向空值,并释放最后一个节点的内存。
  • 删除指定位置的节点:找到要删除位置的前一个节点,将前一个节点的指针指向要删除节点的下一个节点,并释放要删除节点的内存。
    【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第4张图片动图:
    【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第5张图片

(4)查找操作:

  • 根据值查找:从头节点开始遍历链表,比较每个节点的值与目标值,直到找到匹配的节点或遍历到链表末尾。
  • 根据位置查找:从头节点开始遍历链表,按照位置依次查找,直到找到目标位置的节点或遍历到链表末尾。

【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第6张图片

  1. 修改操作:
    根据指定位置或节点,将节点的数据域修改为新的值。

  2. 获取链表长度:
    从头节点开始遍历链表,累计节点的个数,直到遍历到链表末尾。

  3. 遍历链表:
    从头节点开始依次遍历每个节点,可以输出节点的值或进行其他操作。

需要注意的是,在进行插入和删除操作时,要确保链表的指针关系正确,避免出现内存泄漏或指针丢失的情况。

单链表的操作灵活性较高,尤其适用于频繁插入和删除操作的场景。然而,由于无法直接访问前一个节点,某些操作可能需要从头节点开始遍历整个链表,因此在使用时需注意操作的效率。

2.23 C语言实现

带头结点:创建一个链表即创建一个头结点。
不带头结点:创建链表即创建一个节点类型的空指针。

位置:从第一个数据节点开始计算,第一个数据节点位置为0。
长度:数据节点的个数。

下面的代码也可以进一步完善,比如判断malloc内存申请是否成功;当数据有重复时,可以返回所有匹配的元素的位置.

注意处理好节点之间的链接关系就好了。

此外,链表节点使用malloc申请内存,在所有操作完成后,记得释放内存,遍历链表,free即可(我只在第一个代码里面写了,读者注意一下)。

// 释放整个链表内存
void freeList(Node* head) {
    Node* curr = head->next;
    
    while (curr) {
        Node* temp = curr;
        curr = curr->next;
        free(temp);
    }
    
    free(head);
}

▶ 带头结点

// 带头结点的单链表
// 头指针指向头结点,头结点不存放数据
// 计算位置是从第一个数据节点开始计算,0即为第一个数据节点
#include 
#include 

// 定义链表节点
typedef struct Node {   
    int data;    
    struct Node* next;
} Node;

// 创建链表
// 即创建一个头结点,head指针指向头结点,头结点指针为NULL
// 如果没有头结点,那么创建一个链表就至少有一个数据
Node* createList() {
    Node* head = (Node*)malloc(sizeof(Node));
    head->next = NULL;
    return head;
}

// 在指定位置插入节点(前面)
void insertNode(Node* head, int position, int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    
    Node* curr = head;
    int count = 0;
    while (curr && count < position) {
        curr = curr->next;
        count++;
    }
    
    if (curr) {
        newNode->next = curr->next;     // 将新节指针指向插入位置后面的节点
        curr->next = newNode;           // 插入位置的节点指针执行刚刚插入的新节点
    } else {
        printf("Invalid position.\n");
        free(newNode);
    }
}

// 删除指定位置的节点
void deleteNode(Node* head, int position) {
    Node* prev = head;        // 删除位置前一个节点
    Node* curr = head->next;  // 删除位置的节点
    int count = 0;
    
    while (curr && count < position) {
        prev = curr;
        curr = curr->next;
        count++;
    }
    
    if (curr) {
        prev->next = curr->next;   // 将删除位置前一个节点指向删除位置后一个节点
        free(curr);                // 释放删除位置的节点
    } else {
        printf("Invalid position.\n");
    }
}

// 根据值查找节点的位置
// 假设没有重复元素,否则只能返回第一个匹配到的,不过也可以进行修改
int findPositionByValue(Node* head, int value) {
    Node* curr = head->next;  // 从第一个数据节点开始找,位置为0
    int position = 0;
    
    while (curr) {
        if (curr->data == value) {
            return position;
        }
        
        curr = curr->next;
        position++;
    }
    
    return -1;  // 值不存在
}

// 根据位置查找节点的值
int findValueByPosition(Node* head, int position) {
    Node* curr = head->next;
    int count = 0;
    
    while (curr && count < position) {
        curr = curr->next;
        count++;
    }
    
    if (curr) {
        return curr->data;
    } else {
        printf("Invalid position.\n");
        return -1;  // 位置无效
    }
}

// 根据位置修改节点的值
void modifyValueByPosition(Node* head, int position, int newValue) {
    Node* curr = head->next;
    int count = 0;
    
    while (curr && count < position) {
        curr = curr->next;
        count++;
    }
    
    if (curr) {
        curr->data = newValue;
    } else {
        printf("Invalid position.\n");
    }
}

// 获取链表长度(即数据节点的个数)
int getLength(Node* head) {
    Node* curr = head->next;
    int length = 0;
    
    while (curr) {
        curr = curr->next;
        length++;
    }
    
    return length;
}

// 遍历链表
void traverseList(Node* head) {
    Node* curr = head->next;
    
    while (curr)
 {
        printf("%d ", curr->data);
        curr = curr->next;
    }
    
    printf("\n");
}

// 释放整个链表内存
void freeList(Node* head) {
    Node* curr = head->next;
    
    while (curr) {
        Node* temp = curr;
        curr = curr->next;
        free(temp);
    }
    
    free(head);
}

// 测试链表操作
int main() {

    Node* myList = createList();  // 创建链表,即创建一个头结点
    
    insertNode(myList, 0, 1);     //在开头插入1
    insertNode(myList, 1, 2);     
    insertNode(myList, 2, 3);    
    insertNode(myList, 1, 666);
    
    printf("List: ");
    traverseList(myList);   // 遍历打印
    
    printf("Length: %d\n", getLength(myList));  // 求链表长度(数据节点个数)
    
    printf("Position of value 3: %d\n", findPositionByValue(myList, 3));   // 按位置查找
    printf("Value at position 1: %d\n", findValueByPosition(myList, 1));   // 按值查找
    
    modifyValueByPosition(myList, 1, 888);    // 按位置修改  
    
    printf("List after modification: ");       
    traverseList(myList);
    
    deleteNode(myList, 2);              // 按位置删除
    
    printf("List after deletion: ");
    traverseList(myList);
    
    freeList(myList);                  // 释放链表内存
    
    return 0;
} 

运行示例:
【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第7张图片

▶ 不带头结点

// 不带头结点的链表
#include 
#include 

// 定义链表节点
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// 创建链表(是Node*类型)
Node* createList() {
    return NULL;
}

// 在链表头部插入节点
Node* insertNode(Node* head, int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = head;
    return newNode;
}

// 在指定位置插入节点
//  这个函数也可以实现头插
Node* insertNodeAtPosition(Node* head, int position, int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = NULL;
    
    if (position == 0) {
        newNode->next = head;
        return newNode;
    }
    
    Node* curr = head;
    int count = 0;
    
    while (curr && count < position - 1) {
        curr = curr->next;
        count++;
    }
    
    if (curr && count == position - 1) {
        newNode->next = curr->next;
        curr->next = newNode;
    } else {
        printf("Invalid position.\n");
        free(newNode);
    }
    
    return head;
}

// 删除指定位置的节点
Node* deleteNode(Node* head, int position) {
    if (position == 0) {
        Node* temp = head;
        head = head->next;
        free(temp);
        return head;
    }
    
    Node* prev = head;
    Node* curr = head->next;
    int count = 0;
    
    while (curr && count < position - 1) {
        prev = curr;
        curr = curr->next;
        count++;
    }
    
    if (curr && count == position - 1) {
        prev->next = curr->next;
        free(curr);
    } else {
        printf("Invalid position.\n");
    }
    
    return head;
}

// 根据值查找节点的位置
int findPositionByValue(Node* head, int value) {
    Node* curr = head;
    int position = 0;
    
    while (curr) {
        if (curr->data == value) {
            return position;
        }
        
        curr = curr->next;
        position++;
    }
    
    return -1;  // 值不存在
}

// 根据位置查找节点的值
int findValueByPosition(Node* head, int position) {
    Node* curr = head;
    int count = 0;
    
    while (curr && count < position) {
        curr = curr->next;
        count++;
    }
    
    if (curr && count == position) {
        return curr->data;
    } else {
        printf("Invalid position.\n");
        return -1;  // 位置无效
    }
}

// 根据位置修改节点的值
void modifyValueByPosition(Node* head, int position, int newValue) {
    Node* curr = head;
    int count = 0;
    
    while (curr && count < position) {
        curr = curr->next;
        count++;
    }
    
    if (curr && count == position) {
        curr->data = newValue;
    } else {
        printf("Invalid position.\n");
    }
}

// 获取链表长度
   
int getLength(Node* head) {
    Node* curr = head;
    int length = 0;
    
    while (curr) {
        curr = curr->next;
        length++;
    }
    
    return length;
}

// 遍历链表
void traverseList(Node* head) {
    Node* curr = head;
    
    while (curr) {
        printf("%d ", curr->data);
        curr = curr->next;
    }
    
    printf("\n");
}

// 释放链表内存
void freeList(Node* head) {
    Node* curr = head;
    
    while (curr) {
        Node* temp = curr;
        curr = curr->next;
        free(temp);
    }
}

// 测试链表操作
int main() {
    Node* myList = createList();
    
    myList = insertNode(myList, 1);   // 头插
    myList = insertNode(myList, 2);
    myList = insertNode(myList, 3);
    myList=insertNodeAtPosition(myList, 1, 666);  
    myList=insertNodeAtPosition(myList, 0, 888);  // 也可以实现头


    printf("List: ");
    traverseList(myList);
    
    printf("Length: %d\n", getLength(myList));
    
    printf("Position of value 2: %d\n", findPositionByValue(myList, 2));
    printf("Value at position 1: %d\n", findValueByPosition(myList, 1));
    
    modifyValueByPosition(myList, 1, 4);
    
    printf("List after modification: ");
    traverseList(myList);
    
    myList = deleteNode(myList, 2);
    
    printf("List after deletion: ");
    traverseList(myList);
    
    freeList(myList);  // 释放链表内存
    
    return 0;
}

运行:
【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第8张图片

2.3 双向链表

2.31 基本概念

双向链表(Doubly Linked List)是一种常见的数据结构,它允许在链表中的节点之间进行双向遍历。相比于单向链表,双向链表在每个节点中保存了两个指针,分别指向前一个节点和后一个节点。这使得在双向链表中进行插入、删除和遍历操作更加高效灵活。

双向链表由一系列节点组成,每个节点包含两个重要的部分:数据和指针。数据部分用于存储节点所需的信息,而指针部分用于指向前一个节点和后一个节点。在双向链表中,第一个节点被称为头节点,最后一个节点被称为尾节点。头节点的前指针为空,尾节点的后指针为空。
在这里插入图片描述

2.32 与单链表比较

与单链表相比,双向链表的主要优势是在插入、删除和遍历方面具有更高的效率和更大的灵活性。下面是与单链表相比的一些区别和优势:

  1. 双向遍历:双向链表可以从任意一个节点开始,沿着前指针或后指针进行遍历,而单链表只能从头节点开始单向遍历。这使得双向链表在需要从后往前遍历、查找或操作链表的情况下更加方便。

  2. 插入和删除操作效率更高:在双向链表中,插入和删除节点只需要修改相邻节点的指针,而在单链表中,为了插入或删除一个节点,需要找到前一个节点并修改其后指针。这使得双向链表的插入和删除操作更高效,特别是在已知节点位置的情况下,其时间复杂度为O(1),而单链表则需要O(n)的时间复杂度。

  3. 反转链表更方便:由于双向链表中每个节点都存储了前一个节点和后一个节点的指针,因此在双向链表中进行链表反转操作更加方便。只需交换每个节点的前指针和后指针即可实现链表的反转。而在单链表中,为了反转链表,需要使用额外的指针或递归操作,使得实现更加复杂。

  4. 更多的灵活性:双向链表的每个节点都存储了前一个节点和后一个节点的指针,这使得在插入、删除和修改节点时更加灵活。例如,在双向链表中删除一个节点时,只需修改前后节点的指针即可,而在单链表中删除一个节点则需要找到前一个节点并修改其后指针。双向链表的这种灵活性可以使得操作更加简单和高效。

然而,与单链表相比,双向链表需要额外的内存开销来存储前指针,这增加了内存的使用量。另外,双向链表的实现可能稍微复杂一些,因为需要确保前指针和后指针的正确性。因此,在具体应用中,需要根据实际需求和性能要求来选择使用单链表还是双向链表。

2.33 双向链表的应用

双向链表在许多情况下都有广泛的应用,以下是一些双向链表的常见用处:

  1. 高效的插入和删除:相比于数组,双向链表的插入和删除操作更加高效。在双向链表中,插入和删除一个节点只需要修改相邻节点的指针,而不需要像数组那样进行元素的搬移。这使得双向链表在需要频繁插入和删除操作的场景中非常有用。

  2. 双向遍历:双向链表允许从任意一个节点开始,沿着前指针或后指针进行遍历。这使得在某些情况下,从前往后和从后往前遍历链表都很方便。例如,用于实现浏览器的前进和后退功能时,可以使用双向链表存储浏览历史记录,用户可以通过双向遍历来导航。

  3. 缓存实现:双向链表常用于实现缓存。缓存是一种用于加速数据访问的机制,常见的缓存算法如LRU(Least Recently Used)依赖于双向链表的特性。LRU缓存使用双向链表来维护最近使用的数据项顺序,当缓存满时,删除链表尾部的数据项,将新的数据项插入链表头部。这样可以快速定位到最近使用的数据项,并在O(1)的时间复杂度内进行插入和删除操作。

  4. 实现其他数据结构:双向链表是其他复杂数据结构的基础。例如,双向链表常用于实现栈(双向链表头部作为栈顶)、队列(双向链表头部作为队列头)、哈希表中的链表桶等。在这些数据结构中,双向链表的灵活性和高效性使得它成为理想的选择。

  5. 形算法:在图形算法中,双向链表可用于存储图的邻接表。每个节点表示一个图的顶点,通过前指针和后指针与其他相邻的顶点建立连接。这样可以方便地访问一个顶点的邻居顶点,从而实现图的遍历和其他图形算法。

2.34基本操作

(1)插入操作:

  • 在链表头部插入节点:创建一个新节点,将其后指针指向原头节点,将原头节点的前指针指向新节点,更新头节点为新节点。
  • 在链表尾部插入节点:创建一个新节点,将其前指针指向原尾节点,将原尾节点的后指针指向新节点,更新尾节点为新节点。
  • 在链表中间插入节点:创建一个新节点,找到插入位置的前一个节点和后一个节点,将它们的指针指向新节点,更新新节点的指针。
    【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第9张图片
    【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第10张图片

(2)删除操作:

  • 删除头节点:将头节点的后指针指向的节点设为新的头节点,并更新新头节点的前指针为空。
  • 删除尾节点:将尾节点的前指针指向的节点设为新的尾节点,并更新新尾节点的后指针为空。
  • 删除中间节点:找到要删除的节点,修改其前一个节点和后一个节点的指针,将它们连接起来跳过被删除的节点。
    【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第11张图片
    【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第12张图片

(3)查找操作:

  • 从头节点开始遍历:沿着后指针依次遍历节点,直到找到目标节点或到达尾节点。
  • 从尾节点开始遍历:沿着前指针依次遍历节点,直到找到目标节点或到达头节点。

(4) 反转链表:

  • 交换每个节点的前指针和后指针,使得原先指向前一个节点的指针指向后一个节点,原先指向后一个节点的指针指向前一个节点。同时更新头节点和尾节点。

(5) 获取链表长度:

  • 从头节点开始遍历链表,每经过一个节点,计数器加一,直到到达尾节点。

(6)遍历操作:

  • 从头节点开始,沿着后指针依次访问每个节点,可以执行特定的操作,如打印节点的值或进行其他处理。

2.35 C语言实现

▶带头结点的双向链表

// 带头结点的双向老板
#include 
#include 

// 定义双向链表节点结构
typedef struct Node {
    int data;
    struct Node* prev;
    struct Node* next;
} Node;

// 初始化双向链表,即创建头结点
void initializeList(Node** head) {
    *head = (Node*)malloc(sizeof(Node));
    (*head)->data = 0;
    (*head)->prev = NULL;
    (*head)->next = NULL;
}

// 在链表头部插入节点
void insertAtHead(Node* head, int value) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->prev = head;
    newNode->next = head->next;
    if (head->next != NULL) {
        head->next->prev = newNode;
    }
    head->next = newNode;
}

// 在链表尾部插入节点
void insertAtTail(Node* head, int value) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->prev = NULL;
    newNode->next = NULL;
    
    Node* current = head;
    while (current->next != NULL) {
        current = current->next;
    }
    
    current->next = newNode;
    newNode->prev = current;
}

// 在指定位置插入节点
void insertAtPosition(Node* head, int value, int position) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->prev = NULL;
    newNode->next = NULL;
    
    Node* current = head;
    int currentPosition = 0;
    while (current != NULL && currentPosition < position) {
        current = current->next;
        currentPosition++;
    }
    
    if (current == NULL) {
        printf("Invalid position!\n");
        free(newNode);
        return;
    }
    
    newNode->prev = current->prev;
    newNode->next = current;
    current->prev->next = newNode;
    current->prev = newNode;
}

// 删除头节点
void deleteAtHead(Node* head) {
    if (head->next == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    Node* toDelete = head->next;
    head->next = toDelete->next;
    if (toDelete->next != NULL) {
        toDelete->next->prev = head;
    }
    
    free(toDelete);
}

// 删除尾节点
void deleteAtTail(Node* head) {
    if (head->next == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    Node* current = head;
    while (current->next != NULL) {
        current = current->next;
    }
    
    current->prev->next = NULL;
    free(current);
}

// 删除指定位置的节点
void deleteAtPosition(Node* head, int position) {
    if (head->next == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    Node* current = head;
    int currentPosition = 0;
    while (current->next != NULL && currentPosition < position) {
        current = current->next;
        currentPosition++;
    }
    
    if (current->next == NULL) {
        printf("Invalid position!\n");
        return;
    }
    
    Node* toDelete = current->next;
    current->next = toDelete->next;
    if (toDelete->next != NULL) {
        toDelete->next->prev = current;
    }
    
    free(toDelete);
}

// 打印链表
void printList(Node* head) {
    Node* current = head->next;
    while (current != NULL) {
        printf("%d ", current->data);
        current = current->next;
    }
    printf("\n");
}

int main() {
    Node* head;
    initializeList(&head);
    
    insertAtHead(head, 2);
    insertAtHead(head, 1);
    insertAtTail(head, 3);
    insertAtPosition(head, 4, 2);
    
    printf("List: ");
    printList(head);
    
    deleteAtHead(head);
    deleteAtTail(head);
    deleteAtPosition(head, 1);
    
    printf("List after deletion: ");
    printList(head);
    
    return 0;
}  

运行:
【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第13张图片

▶不带头结点的双向链表

// 不带头结点的双向链表
#include 
#include 

// 定义双向链表节点结构
typedef struct Node {
    int data;
    struct Node* prev;
    struct Node* next;
} Node;

// 初始化双向链表
Node* initializeList() {
    return NULL;
}

// 在链表头部插入节点
Node* insertAtHead(Node* head, int value) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->prev = NULL;
    newNode->next = head;
    if (head != NULL) {
        head->prev = newNode;
    }
    return newNode;
}

// 在链表尾部插入节点
Node* insertAtTail(Node* head, int value) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->prev = NULL;
    newNode->next = NULL;
    
    if (head == NULL) {
        return newNode;
    }
    
    Node* current = head;
    while (current->next != NULL) {
        current = current->next;
    }
    
    current->next = newNode;
    newNode->prev = current;
    
    return head;
}

// 在指定位置插入节点
Node* insertAtPosition(Node* head, int value, int position) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->prev = NULL;
    newNode->next = NULL;
    
    if (position <= 0) {
        newNode->next = head;
        if (head != NULL) {
            head->prev = newNode;
        }
        return newNode;
    }
    
    Node* current = head;
    int currentPosition = 0;
    while (current != NULL && currentPosition < position) {
        current = current->next;
        currentPosition++;
    }
    
    if (current == NULL) {
        printf("Invalid position!\n");
        free(newNode);
        return head;
    }
    
    newNode->prev = current->prev;
    newNode->next = current;
    
    if (current->prev != NULL) {
        current->prev->next = newNode;
    }
    current->prev = newNode;
    
    if (currentPosition == 0) {
        return newNode;
    }
    
    return head;
}

// 删除头节点
Node* deleteAtHead(Node* head) {
    if (head == NULL) {
        printf("List is empty!\n");
        return NULL;
    }
    
    Node* toDelete = head;
    head = head->next;
    if (head != NULL) {
        head->prev = NULL;
    }
    
    free(toDelete);
    return head;
}

// 删除尾节点
Node* deleteAtTail(Node* head) {
    if (head == NULL) {
        printf("List is empty!\n");
        return NULL;
    }
    
    if (head->next == NULL) {
        free(head);
        return NULL;
    }
    
    Node* current = head;
    while (current->next != NULL) {
        current = current->next;
    }
    
    current->prev->next = NULL;
    free(current);
    return head;
}

// 删除指定位置的节点
Node* deleteAtPosition(Node* head, int position) {
    if (head == NULL) {
        printf("List is empty!\n");
        return NULL;
    }
        if (position <= 0) {
        Node* toDelete = head;
        head = head->next;
        if (head != NULL) {
            head->prev = NULL;
        }
        free(toDelete);
        return head;
    }
    
    Node* current = head;
    int currentPosition = 0;
    while (current != NULL && currentPosition < position) {
        current = current->next;
        currentPosition++;
    }
    
    if (current == NULL) {
        printf("Invalid position!\n");
        return head;
    }
    
    current->prev->next = current->next;
    
    if (current->next != NULL) {
        current->next->prev = current->prev;
    }
    
    free(current);
    return head;
}

// 打印链表
void printList(Node* head) {
    Node* current = head;
    while (current != NULL) {
        printf("%d ", current->data);
        current = current->next;
    }
    printf("\n");
}

int main() {
    Node* head = initializeList();
    
    head = insertAtHead(head, 2);
    head = insertAtHead(head, 1);
    head = insertAtTail(head, 3);
    head = insertAtPosition(head, 4, 2);
    
    printf("List: ");
    printList(head);
    
    head = deleteAtHead(head);
    head = deleteAtTail(head);
    head = deleteAtPosition(head, 1);
    
    printf("List after deletion: ");
    printList(head);
    
    return 0;
} 

运行:
【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第14张图片

2.4 循环链表

2.41 基本概念

循环链表是一种特殊类型的链表,与常规链表不同之处在于,循环链表的尾节点指向头节点,形成一个闭环。这意味着循环链表中的每个节点都有一个指针指向下一个节点,并且最后一个节点指向第一个节点。这种特性使得循环链表可以像常规链表一样遍历和访问节点,同时也可以更方便地实现循环操作。

循环链表可以分为两种类型:单向循环链表双向循环链表。单向循环链表中的节点只包含一个指向下一个节点的指针,而双向循环链表中的节点则包含一个指向前一个节点和一个指向下一个节点的指针。

单向循环链表:

在这里插入图片描述

在用头指针指示的循环单链表中,找到开始结点的时间是 O(1),然而要找到终端结点,则需从头指针开始遍历整个链表,其时间是 O(n)。在很多实际问题中,操作是在表的首或尾两端进行的,此时头指针指示的循环单链表就显得不够方便。如果改用指向终端结点的尾指针(rearpointer)来指示循环单链表,则查找开始结点和终端结点都很方便,它们的存储地址分别是(rear -> next)-> next和 rear,显然,时间都是O(1)。因此,实际应用中多采用尾指针指示的循环单链表。

在这里插入图片描述

双向循环链表:

在这里插入图片描述

循环链表的特点和优势:

  1. 循环链表可以无限循环,从任意节点出发都能遍历整个链表,不需要通过头节点来获取链表的起始位置。
  2. 在某些问题中,循环链表可以提供更简洁的解决方案。例如,可以使用循环链表来模拟循环队列或循环缓冲区等数据结构。
  3. 循环链表可以减少存储空间的浪费。由于循环链表中没有空指针,所以没有节点被孤立,不会浪费额外的空间。
  4. 循环链表的插入和删除操作相对简单高效,不需要处理头节点或尾节点的特殊情况。

2.42 循环链表的基本操作

循环链表的基本操作包括以下几种:

  1. 创建循环链表:分配内存并初始化一个节点,将其指针指向自身形成循环,然后将头指针指向该节点。
  2. 在头部插入节点:创建一个新节点,将新节点的指针指向当前头节点,然后更新头指针和尾指针的指向。
  3. 在尾部插入节点:创建一个新节点,将新节点的指针指向头节点,然后更新尾指针的指向。
  4. 在指定位置插入节点:创建一个新节点,找到指定位置的节点,将新节点的指针指向下一个节点,然后更新前一个节点和当前节点的指向。
  5. 删除头节点:将头指针指向下一个节点,然后更新尾指针的指向。
  6. 删除尾节点:找到尾节点的前一个节点,将其指针指向头节点,然后更新尾指针的指向。
  7. 删除指定位置的节点:找到指定位置的节点,将前一个节点的指针指向下一个节点,然后释放被删除节点的内存空间。
  8. 遍历循环链表:从头节点开始,依次访问每个节点,直到再次回到头节点。

2.43 循环链表应用场景

循环链表由于其特殊的闭环结构,可以在许多场景下提供便利和高效的解决方案。以下是一些常见的应用场景:

  1. 缓冲区:循环链表可以用于实现缓冲区(buffer),其中数据按照循环方式存储和访问。例如,在音频或视频流处理中,可以使用循环链表来存储连续的数据块,以实现循环缓冲区的功能。

  2. 线程调度:在操作系统中,循环链表可以用于实现进程或线程的调度算法。每个进程或线程可以表示为链表中的一个节点,调度算法可以根据一定的策略在各个节点之间进行切换。

  3. 约瑟夫问题:约瑟夫问题是一个经典的数学问题,涉及到一组人围成一个圆圈报数,每报到特定数目的人就被淘汰,直到只剩下最后一个人。循环链表可以用于解决约瑟夫问题,通过不断删除节点来模拟淘汰过程。

  4. 循环队列:循环队列是一种基于循环链表的数据结构,用于解决队列元素的循环利用问题。它在一端插入元素,在另一端删除元素,并且可以循环利用队列空间,提高存储效率。

  5. 轮播图:在网页设计中,循环链表可以用于实现轮播图效果,其中每个图片或广告可以表示为链表中的一个节点,通过不断循环遍历链表来实现图片的轮播展示。

  6. 密码学:在密码学中,循环链表可以用于实现一些加密算法,例如循环移位密码(Caesar Cipher)或循环替换密码(Polyalphabetic Cipher)等。

这只是循环链表应用的一小部分示例,实际上,在许多需要循环或环形结构的场景中,循环链表都可以提供简洁、高效的解决方案。

2.44 C语言实现

单向循环链表、双向循环链表,各自又分为带不带头结点2种。

▶ 带头结点的单向循环链表

#include 
#include 

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

// 初始化链表,创建头节点
Node* initializeList() {
    Node* head = (Node*)malloc(sizeof(Node));
    head->next = head; // 头节点指向自身形成循环
    return head;
}

// 在链表尾部插入节点
void insertAtTail(Node* head, int value) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->next = head->next; // 新节点指向头节点
    head->next = newNode; // 头节点指向新节点
    head = newNode; // 更新头节点
}

// 在指定位置插入节点
void insertAtPosition(Node* head, int value, int position) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    Node* current = head;
    int currentPosition = 0;
    while (current->next != head && currentPosition < position - 1) {
        current = current->next;
        currentPosition++;
    }
    newNode->next = current->next;
    current->next = newNode;
}

// 删除指定位置的节点
void deleteAtPosition(Node* head, int position) {
    Node* current = head;
    Node* toDelete = NULL;
    int currentPosition = 0;
    while (current->next != head && currentPosition < position - 1) {
        current = current->next;
        currentPosition++;
    }
    toDelete = current->next;
    current->next = toDelete->next;
    free(toDelete);
}

// 打印链表
void printList(Node* head) {
    Node* current = head->next;
    while (current != head) {
        printf("%d ", current->data);
        current = current->next;
    }
    printf("\n");
}

int main() {
    Node* head = initializeList();
    
    insertAtTail(head, 1);
    insertAtTail(head, 2);
    insertAtTail(head, 3);
    insertAtPosition(head, 4, 2);
    
    printf("List: ");
    printList(head);
    
    deleteAtPosition(head, 1);
    
    printf("List after deletion: ");
    printList(head);
    
    return 0;
} 

▶ 不带头结点的单向循环链表

#include 
#include 

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

// 初始化链表,创建第一个节点
Node* initializeList(int value) {
    Node* head = (Node*)malloc(sizeof(Node));
    head->data = value;
    head->next = head; // 第一个节点指向自身形成循环
    return head;
}

// 在链表尾部插入节点
void insertAtTail(Node* head, int value) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->next = head; // 新节点指向头节点
    Node* current = head;
    while (current->next != head) {
        current = current->next;
    }
    current->next = newNode; // 将新节点连接到尾节点之后
}

// 在指定位置插入节点
void insertAtPosition(Node* head, int value, int position) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    Node* current = head;
    int currentPosition = 0;
    while (current->next != head && currentPosition < position - 1) {
        current = current->next;
        currentPosition++;
    }
    newNode->next = current->next;
    current->next = newNode;
}

// 删除指定位置的节点
void deleteAtPosition(Node* head, int position) {
    Node* current = head;
    Node* toDelete = NULL;
    int currentPosition = 0;
    while (current->next != head && currentPosition < position - 1) {
        current = current->next;
        currentPosition++;
    }
    toDelete = current->next;
    current->next = toDelete->next;
    free(toDelete);
}

// 打印链表
void printList(Node* head) {
    Node* current = head;
    do {
        printf("%d ", current->data);
        current = current->next;
    } while (current != head);
    printf("\n");
}

int main() {
    Node* head = initializeList(1);
    
    insertAtTail(head, 2);
    insertAtTail(head, 3);
    insertAtPosition(head, 4, 2);
    
    printf("List: ");
    printList(head);
    
    deleteAtPosition(head, 1);
    
    printf("List after deletion: ");
    printList(head);
    
    return 0;
}

▶ 带头结点的双向循环链表

#include 
#include 

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

// 初始化链表,创建头节点
Node* initializeList() {
    Node* head = (Node*)malloc(sizeof(Node));
    head->prev = head;
    head->next = head; // 头节点的前后指针都指向自身形成循环
    return head;
}

// 在链表尾部插入节点
void insertAtTail(Node* head, int value) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->prev = head->prev;
    newNode->next = head;
    head->prev->next = newNode; // 新节点插入到尾节点之后
    head->prev = newNode; // 更新尾节点
}

// 在指定位置插入节点
void insertAtPosition(Node* head, int value, int position) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    Node* current = head;
    int currentPosition = 0;
    while (current->next != head && currentPosition < position) {
        current = current->next;
        currentPosition++;
    }
    newNode->prev = current;
    newNode->next = current->next;
    current->next->prev = newNode;
    current->next = newNode;
}

// 删除指定位置的节点
void deleteAtPosition(Node* head, int position) {
    Node* current = head;
    Node* toDelete = NULL;
    int currentPosition = 0;
    while (current->next != head && currentPosition < position) {
        current = current->next;
        currentPosition++;
    }
    toDelete = current->next;
    current->next = toDelete->next;
    toDelete->next->prev = current;
    free(toDelete);
}

// 打印链表
void printList(Node* head) {
    Node* current = head->next;
    while (current != head) {
        printf("%d ", current->data);
        current = current->next;
    }
    printf("\n");
}

int main() {
    Node* head = initializeList();
    
    insertAtTail(head, 1);
    insertAtTail(head, 2);
    insertAtTail(head, 3);
    insertAtPosition(head, 4, 2);
    
    printf("List: ");
    printList(head);
    
    deleteAtPosition(head, 1);
    
    printf("List after deletion: ");
    printList(head);
    
    return 0;
}

▶ 不带头结点的双向循环链表

#include 
#include 

typedef struct Node {
    int data;
    struct Node* prev;
    struct Node* next;
} Node;

Node* initializeList(int value) {
    Node* head = (Node*)malloc(sizeof(Node));
    head->data = value;
    head->prev = head;
    head->next = head;
    return head;
}

void insertAtTail(Node* head, int value) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->prev = head->prev;
    newNode->next = head;
    head->prev->next = newNode;
    head->prev = newNode;
}

void insertAtPosition(Node* head, int value, int position) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    Node* current = head->next;
    int currentPosition = 0;
    while (current != head && currentPosition < position) {
        current = current->next;
        currentPosition++;
    }
    newNode->next = current;
    newNode->prev = current->prev;
    current->prev->next = newNode;
    current->prev = newNode;
}

void deleteAtPosition(Node* head, int position) {
    Node* current = head->next;
    int currentPosition = 0;
    while (current != head && currentPosition < position) {
        current = current->next;
        currentPosition++;
    }
    if (current != head) {
        current->prev->next = current->next;
        current->next->prev = current->prev;
        free(current);
    }
}

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

int main() {
    Node* head = initializeList(1);
    printf("初始化OK");
    
    insertAtTail(head, 2);
    insertAtTail(head, 3);
    insertAtPosition(head, 4, 2);
    
    printf("List: ");
    printList(head);
    
    deleteAtPosition(head, 1);
    
    printf("List after deletion: ");
    printList(head);
    
    return 0;
}

三、一些高阶操作

3.1 链表反转

3.11 单链表反转

要反转单链表,可以使用迭代或递归的方法。

  1. 迭代方法:
    • 初始化三个指针:prev(指向前一个节点)、current(指向当前节点)、next(指向下一个节点)。
    • 遍历链表,将当前节点的下一个节点保存在 next 指针中,然后将当前节点的 next 指针指向前一个节点,即将链表方向反转。
    • 移动 prev 和 current 指针到下一个节点,并将 next 指针作为当前节点继续遍历链表。
    • 当 current 指针指向最后一个节点时,链表反转完成,将最后一个节点设为新的头节点。
    • 返回新的头节点。

下面是使用迭代方法反转单链表的示例代码(带不带头结点,自己稍微处理一下):

Node* reverseList(Node* head) {
    Node* prev = NULL;
    Node* current = head;
    Node* next = NULL;

    while (current != NULL) {
        next = current->next;
        current->next = prev;
        prev = current;
        current = next;
    }

    return prev; // prev指向原链表的最后一个节点,现在是反转后链表的头节点
}
  1. 递归方法:
    • 基本情况:如果链表为空或者只有一个节点,直接返回该节点。
    • 递归调用:递归反转剩余部分的链表,得到新的头节点。
    • 连接节点:将当前节点的下一个节点的 next 指针指向当前节点,实现反转。
    • 清除原始连接:将当前节点的 next 指针设为 NULL,断开原始连接。
    • 返回新的头节点。

下面是使用递归方法反转单链表的示例代码:

Node* reverseList(Node* head) {
    if (head == NULL || head->next == NULL) {
        return head;
    }

    Node* newHead = reverseList(head->next);
    head->next->next = head;
    head->next = NULL;

    return newHead;
}

不带头结点,递归反转:
【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第15张图片

3.12 双向链表反转

要反转双向链表,可以使用类似的迭代或递归的方法。

  1. 迭代方法:
    • 初始化三个指针:prev(指向前一个节点)、current(指向当前节点)、next(指向下一个节点)。
    • 遍历链表,将当前节点的前一个节点保存在 prev 指针中,当前节点的 next 指针指向前一个节点,当前节点的 prev 指针指向下一个节点,即将链表的前后关系反转。
    • 移动 prev、current 和 next 指针到下一个节点,并继续遍历链表。
    • 当 current 指针指向最后一个节点时,链表反转完成,将最后一个节点设为新的头节点。
    • 返回新的头节点。

下面是使用迭代方法反转双向链表的示例代码:

Node* reverseList(Node* head) {
    Node* prev = NULL;
    Node* current = head;
    Node* next = NULL;

    while (current != NULL) {
        next = current->next;
        current->next = prev;
        current->prev = next;
        prev = current;
        current = next;
    }

    return prev; // prev指向原链表的最后一个节点,现在是反转后链表的头节点
}
  1. 递归方法:
    • 基本情况:如果链表为空或者只有一个节点,直接返回该节点。
    • 递归调用:递归反转剩余部分的链表,得到新的头节点。
    • 交换节点:将当前节点的 prev 指针和 next 指针交换,实现反转。
    • 返回新的头节点。

下面是使用递归方法反转双向链表的示例代码:

Node* reverseList(Node* head) {
    if (head == NULL || head->next == NULL) {
        return head;
    }

    Node* newHead = reverseList(head->next);
    head->next->prev = head->next->next;
    head->next->next = head;
    head->prev = head->next;
    head->next = NULL;

    return newHead;
}

通过迭代或递归方法,可以有效地反转双向链表。根据具体情况选择合适的方法进行实现。


循环链表一般不需要反转。

3.2 链表排序

应用:

  • 数据检索;
  • 数据展示;
  • 数据去重;
  • 数据统计;

要对单向链表进行排序,常见的方法是使用排序算法,如冒泡排序、插入排序、选择排序、归并排序或快速排序。这些排序算法可以通过对链表节点之间的值进行比较和交换来实现排序。

以下是使用归并排序对单向链表进行排序的示例代码:

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

// 获取链表的中间节点
Node* getMiddle(Node* head) {
    if (head == NULL)
        return head;

    Node* slow = head;
    Node* fast = head;

    while (fast->next != NULL && fast->next->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }

    return slow;
}

// 合并两个有序链表
Node* merge(Node* head1, Node* head2) {
    if (head1 == NULL)
        return head2;
    if (head2 == NULL)
        return head1;

    Node* mergedHead = NULL;

    if (head1->data <= head2->data) {
        mergedHead = head1;
        mergedHead->next = merge(head1->next, head2);
    } else {
        mergedHead = head2;
        mergedHead->next = merge(head1, head2->next);
    }

    return mergedHead;
}

// 归并排序
Node* mergeSort(Node* head) {
    if (head == NULL || head->next == NULL)
        return head;

    Node* middle = getMiddle(head);
    Node* nextToMiddle = middle->next;
    middle->next = NULL;

    Node* left = mergeSort(head);
    Node* right = mergeSort(nextToMiddle);

    return merge(left, right);
}

使用归并排序时,首先通过 getMiddle 函数找到链表的中间节点,然后将链表分为两部分。接下来递归地对两部分链表进行排序,然后使用 merge 函数合并排序后的链表。最后返回排序后的链表。

请注意,在使用归并排序时,可能需要使用递归方法,因此要注意对于大型链表可能导致栈溢出。在实际使用中,可以根据链表的长度和具体需求选择适当的排序算法。

3.3 约瑟夫环问题

3.31 问题重述

约瑟夫环(Josephus problem)是一个经典的数学问题,它得名于古代历史学家和数学家约瑟夫斯(Flavius Josephus)。问题的描述如下:

有 n 个人(编号为 1 到 n)围成一个圆圈,从某个人开始顺时针报数,报到 m 的人出列,然后从下一个人重新开始报数,直到所有人都出列。求最后剩下的人的编号。

例如,假设有 7 个人围成一个圆圈,报数到 3 的人出列,那么出列的顺序是 3、6、2、7、5、1,最后剩下的人的编号是 4。

约瑟夫环问题可以使用递归或数学推导来解决。通过推导可以得到一个递推公式:

f ( n , m ) = ( f ( n − 1 , m ) + m ) f(n, m) = (f(n-1, m) + m) % n f(n,m)=(f(n1,m)+m)

其中 f(n, m) 表示 n 个人报数到 m 时最后剩下的人的编号。

通过递归或循环计算,可以解决约瑟夫环问题。这个问题在计算机科学和数学领域具有一定的研究和应用价值,同时也是一个有趣的智力游戏和谜题。

3.32 链表解决

  1. 定义链表节点的结构体,包含一个整数类型的成员用于存储人的编号,以及一个指向下一个节点的指针。
typedef struct Node {
    int data;
    struct Node* next;
} Node;
  1. 创建一个循环链表,将 n 个人的编号依次插入到链表中。每个人对应一个链表节点,并将最后一个节点的 next 指针指向第一个节点,形成循环。
Node* createCircularList(int n) {
    Node* head = NULL;
    Node* tail = NULL;

    for (int i = 1; i <= n; i++) {
        Node* newNode = (Node*)malloc(sizeof(Node));
        newNode->data = i;
        newNode->next = NULL;

        if (head == NULL) {
            head = newNode;
            tail = newNode;
        } else {
            tail->next = newNode;
            tail = newNode;
        }
    }

    tail->next = head;  // 将最后一个节点的 next 指针指向头节点,形成循环

    return head;
}
  1. 定义一个函数来模拟约瑟夫环的报数和出列过程。遍历链表,按照报数规则将节点从链表中删除,直到只剩下一个节点。
int josephusCircle(Node* head, int m) {
    Node* current = head;
    Node* prev = NULL;

    while (current->next != current) {
        // 报数 m-1 次,找到要出列的节点
        for (int i = 0; i < m - 1; i++) {
            prev = current;
            current = current->next;
        }

        // 删除当前节点
        prev->next = current->next;
        Node* toDelete = current;
        current = prev->next;
        free(toDelete);
    }

    return current->data;  // 返回最后剩下的节点的编号
}
  1. 在主函数中调用以上定义的函数,传入人数 n 和报数值 m,输出最后剩下的人的编号。
int main() {
    int n = 7;  // 人数
    int m = 3;  // 报数值

    Node* head = createCircularList(n);
    int lastPerson = josephusCircle(head, m);

    printf("The last person remaining is %d\n", lastPerson);

    return 0;
}

【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第16张图片

3.4 块状链表

3.41 概念

块状链表(Block-linked list),用于高效地管理大量小对象的分配和释放。它的设计目标是减少内存碎片化,并提高分配和释放操作的性能。

在传统的链表数据结构中,每个节点都包含一个指向下一个节点的指针。而在块状链表中,节点被组织成固定大小的块,每个块包含多个节点。每个块都有一个指向下一个块的指针,形成了一个链表结构。块状链表通常使用位图或其他方法来跟踪节点的使用状态。

简单来说就是这样:

typedef struct Block {
    Node nodes[BLOCK_SIZE];
    struct Block* next;
} Block;

直观来说就是这样:
【数据结构与算法】 01 链表 (单链表、双向链表、循环链表、块状链表、头结点、链表反转与排序、约瑟夫环问题)_第17张图片

块状链表的主要优点是减少了指针的数量,从而节省了内存空间。由于块中包含多个节点,节点之间的空间利用率更高,减少了内存碎片化的问题。此外,由于块内节点的连续存储,对于分配和释放操作,可以更高效地操作整个块,而不是单个节点,提高了性能。

块状链表在许多应用中都有广泛的使用,特别是在嵌入式系统、操作系统内存管理以及高性能计算等领域。它们通常用于管理大量的小对象,例如文件系统中的磁盘块、内存管理中的页表等。

需要注意的是,块状链表的设计和实现相对复杂,需要处理块的分配、释放、合并等操作。这些操作的具体实现可能因不同的应用和要求而有所不同。

3.42 C语言实现

#include 
#include 

#define BLOCK_SIZE 4  // 每个块中节点的数量

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

// 定义块结构体
typedef struct Block {
    Node nodes[BLOCK_SIZE];
    struct Block* next;
} Block;

// 初始化块
Block* initializeBlock() {
    Block* block = (Block*)malloc(sizeof(Block));
    block->next = NULL;
    return block;
}

// 从块中分配一个节点
Node* allocateNode(Block* block) {
    for (int i = 0; i < BLOCK_SIZE; i++) {
        if (block->nodes[i].next == NULL) {
            return &(block->nodes[i]);
        }
    }
    return NULL;  // 没有可用的节点
}

// 释放节点到块中
void deallocateNode(Block* block, Node* node) {
    node->next = NULL;
}

// 向链表中插入节点
void insertNode(Block** head, int value) {
    Block* currentBlock = *head;
    Node* newNode = NULL;

    while (currentBlock != NULL) {
        newNode = allocateNode(currentBlock);
        if (newNode != NULL) {
            break;
        }
        currentBlock = currentBlock->next;
    }

    if (newNode == NULL) {
        // 没有可用的节点,需要分配新的块
        Block* newBlock = initializeBlock();
        newNode = allocateNode(newBlock);

        // 将新的块插入链表
        newBlock->next = *head;
        *head = newBlock;
    }

    newNode->data = value;
}

// 打印链表
void printList(Block* head) {
    Block* currentBlock = head;
    Node* currentNode = NULL;

    while (currentBlock != NULL) {
        for (int i = 0; i < BLOCK_SIZE; i++) {
            currentNode = &(currentBlock->nodes[i]);
            if (currentNode == NULL) {
                break;
            }
            printf("%d ", currentNode->data);
        }
        currentBlock = currentBlock->next;
    }
    printf("\n");
}

// 释放链表内存
void freeList(Block* head) {
    Block* currentBlock = head;
    Block* nextBlock = NULL;
    Node* currentNode = NULL;

    while (currentBlock != NULL) {
        nextBlock = currentBlock->next;
        for (int i = 0; i < BLOCK_SIZE; i++) {
            currentNode = &(currentBlock->nodes[i]);
            currentNode->next = NULL;
        }
        free(currentBlock);
        currentBlock = nextBlock;
    }
}

int main() {
    Block* head = initializeBlock();
    insertNode(&head, 1);
    insertNode(&head, 2);
    insertNode(&head, 3);
    insertNode(&head, 4);
    insertNode(&head, 5);

    printf("List: ");
    printList(head);

    freeList(head);

    return 0;
}





~

你可能感兴趣的:(C,Language,算法小抄,链表,数据结构,算法,c语言)