当前位置: 首页 > news >正文

算法笔记 04

1 c语言创建单链表

#include <stdio.h>
#include <stdlib.h>// 定义链表节点结构体
struct ListNode {int val;struct ListNode* next;
};// 创建单链表的函数(简化版)
struct ListNode* createLinkedList(int* arr, int size) {if (size == 0) return NULL;struct ListNode* head = (struct ListNode*)malloc(sizeof(struct ListNode));head->val = arr[0];head->next = NULL;struct ListNode* cur = head;for (int i = 1; i < size; i++) {cur->next = (struct ListNode*)malloc(sizeof(struct ListNode));cur->next->val = arr[i];cur->next->next = NULL;cur = cur->next;}return head;
}int main() {int arr[] = {1, 2, 3, 4, 5};int size = sizeof(arr) / sizeof(arr[0]);struct ListNode* head = createLinkedList(arr, size);// 遍历单链表for (struct ListNode* p = head; p != NULL; p = p->next) {printf("%d\n", p->val);}// 释放链表内存(此处省略,实际使用中需添加)return 0;
}

2 双链表

创建双链表

双链表(双向链表)是链表的一种扩展形式,它的每个节点除了存储自身数据外,还包含两个指针(或引用):

  • 一个指向下一个节点next
  • 一个指向上一个节点prev

这种结构相比单链表(只有next指针)的优势是:可以双向遍历链表(既能从头部遍历到尾部,也能从尾部遍历到头部),并且在已知节点的情况下,删除或插入节点的操作更高效(无需像单链表那样从头遍历寻找前驱节点)。

用 C 语言实现双链表的示例代码:

#include <stdio.h>
#include <stdlib.h>// 定义双链表节点结构体
struct DoublyListNode {int val;                  // 节点存储的数据struct DoublyListNode* next;  // 指向后一个节点的指针struct DoublyListNode* prev;  // 指向前一个节点的指针
};// 创建双链表的函数
struct DoublyListNode* createDoublyLinkedList(int* arr, int size) {if (arr == NULL || size == 0) {return NULL;}// 创建头节点struct DoublyListNode* head = (struct DoublyListNode*)malloc(sizeof(struct DoublyListNode));head->val = arr[0];head->prev = NULL;  // 头节点的prev为NULLhead->next = NULL;struct DoublyListNode* cur = head;  // 当前节点指针,初始指向头节点// 循环创建后续节点for (int i = 1; i < size; i++) {// 创建新节点struct DoublyListNode* newNode = (struct DoublyListNode*)malloc(sizeof(struct DoublyListNode));newNode->val = arr[i];newNode->next = NULL;  // 新节点暂时没有后驱// 连接当前节点和新节点cur->next = newNode;   // 当前节点的next指向新节点newNode->prev = cur;   // 新节点的prev指向当前节点// 移动当前节点指针到新节点cur = cur->next;}return head;
}// 测试:遍历双链表(正序和逆序)
int main() {int arr[] = {1, 2, 3, 4, 5};int size = sizeof(arr) / sizeof(arr[0]);struct DoublyListNode* head = createDoublyLinkedList(arr, size);// 正序遍历(从头部到尾部)printf("正序遍历: ");struct DoublyListNode* p = head;while (p != NULL) {printf("%d ", p->val);p = p->next;}printf("\n");// 逆序遍历(从尾部到头部,先找到尾节点)printf("逆序遍历: ");struct DoublyListNode* tail = head;while (tail->next != NULL) {tail = tail->next;}while (tail != NULL) {printf("%d ", tail->val);tail = tail->prev;}printf("\n");// 注意:实际使用中需释放链表内存,避免内存泄漏(此处省略)return 0;
}

双链表的核心特点:

  1. 双向遍历:通过next指针可以正向遍历,通过prev指针可以反向遍历(如示例中的正序和逆序输出)。
  2. 节点关系明确:每个节点都能直接找到自己的前驱和后继,操作更灵活。
  3. 空间开销略大:相比单链表,每个节点多一个prev指针,会占用更多内存。

双链表在需要频繁双向操作的场景中非常实用(例如实现双向队列、浏览器的前进 / 后退功能等)。

双链表的增删改查

以下是 C 语言实现双链表的完整代码,包含创建、插入(增)、删除(删)、修改(改)、查询(查) 等核心操作,并附带示例测试:

双链表的定义与核心操作

#include <stdio.h>
#include <stdlib.h>// 定义双链表节点结构
struct DoublyListNode {int val;                     // 节点值struct DoublyListNode* prev; // 前驱节点指针struct DoublyListNode* next; // 后继节点指针
};// 1. 创建双链表(从数组初始化)
struct DoublyListNode* createDoublyList(int* arr, int size) {if (arr == NULL || size <= 0) return NULL;// 创建头节点struct DoublyListNode* head = (struct DoublyListNode*)malloc(sizeof(struct DoublyListNode));head->val = arr[0];head->prev = NULL;head->next = NULL;struct DoublyListNode* cur = head;for (int i = 1; i < size; i++) {struct DoublyListNode* newNode = (struct DoublyListNode*)malloc(sizeof(struct DoublyListNode));newNode->val = arr[i];newNode->prev = cur;   // 新节点的前驱指向当前节点newNode->next = NULL;cur->next = newNode;   // 当前节点的后继指向新节点cur = cur->next;       // 移动到新节点}return head;
}// 2. 查找节点(根据值查找第一个匹配的节点)
struct DoublyListNode* findNode(struct DoublyListNode* head, int target) {struct DoublyListNode* cur = head;while (cur != NULL) {if (cur->val == target) {return cur; // 找到返回节点指针}cur = cur->next;}return NULL; // 未找到返回NULL
}// 3. 修改节点值(根据值修改第一个匹配的节点)
int updateNode(struct DoublyListNode* head, int oldVal, int newVal) {struct DoublyListNode* node = findNode(head, oldVal);if (node == NULL) {return 0; // 未找到节点,修改失败}node->val = newVal;return 1; // 修改成功
}// 4. 插入节点(在指定值的节点后插入新节点)
int insertAfter(struct DoublyListNode* head, int target, int newVal) {struct DoublyListNode* targetNode = findNode(head, target);if (targetNode == NULL) {return 0; // 未找到目标节点,插入失败}// 创建新节点struct DoublyListNode* newNode = (struct DoublyListNode*)malloc(sizeof(struct DoublyListNode));newNode->val = newVal;// 连接新节点与前后节点newNode->next = targetNode->next; // 新节点的后继指向目标节点的原后继newNode->prev = targetNode;       // 新节点的前驱指向目标节点if (targetNode->next != NULL) {   // 如果目标节点不是尾节点targetNode->next->prev = newNode; // 原后继节点的前驱指向新节点}targetNode->next = newNode;       // 目标节点的后继指向新节点return 1; // 插入成功
}// 5. 删除节点(删除第一个匹配值的节点)
int deleteNode(struct DoublyListNode** head, int target) {if (head == NULL || *head == NULL) {return 0; // 链表为空,删除失败}struct DoublyListNode* cur = *head;// 查找目标节点while (cur != NULL && cur->val != target) {cur = cur->next;}if (cur == NULL) {return 0; // 未找到目标节点}// 处理前驱节点的后继指针if (cur->prev != NULL) {cur->prev->next = cur->next;} else {// 若删除的是头节点,更新头指针*head = cur->next;}// 处理后继节点的前驱指针if (cur->next != NULL) {cur->next->prev = cur->prev;}free(cur); // 释放节点内存return 1; // 删除成功
}// 辅助函数:打印链表(正序)
void printList(struct DoublyListNode* head) {struct DoublyListNode* cur = head;while (cur != NULL) {printf("%d ", cur->val);cur = cur->next;}printf("\n");
}// 辅助函数:释放链表内存
void freeList(struct DoublyListNode* head) {struct DoublyListNode* cur = head;while (cur != NULL) {struct DoublyListNode* temp = cur;cur = cur->next;free(temp);}
}// 测试示例
int main() {int arr[] = {1, 2, 3, 4, 5};int size = sizeof(arr) / sizeof(arr[0]);// 创建双链表struct DoublyListNode* head = createDoublyList(arr, size);printf("初始链表: ");printList(head); // 输出:1 2 3 4 5// 查找节点struct DoublyListNode* node = findNode(head, 3);if (node != NULL) {printf("找到节点: %d\n", node->val); // 输出:找到节点: 3}// 修改节点(将3改为30)updateNode(head, 3, 30);printf("修改后链表: ");printList(head); // 输出:1 2 30 4 5// 插入节点(在2后插入25)insertAfter(head, 2, 25);printf("插入后链表: ");printList(head); // 输出:1 2 25 30 4 5// 删除节点(删除4)deleteNode(&head, 4);printf("删除后链表: ");printList(head); // 输出:1 2 25 30 5// 释放内存freeList(head);return 0;
}

核心操作说明

  1. 创建(createDoublyList:从数组初始化双链表,每个节点的 prev 和 next 指针正确关联。

  2. 查询(findNode:遍历链表,返回第一个值为 target 的节点指针(未找到返回 NULL)。

  3. 修改(updateNode:先通过 findNode 找到目标节点,再修改其 val 值(返回 1 表示成功,0 表示失败)。

  4. 插入(insertAfter:在值为 target 的节点后插入新节点,需同时处理前驱和后继的指针关联(若目标节点是尾节点,只需处理前驱)。

  5. 删除(deleteNode:删除值为 target 的节点,需注意:

    • 若删除头节点,需更新头指针(因此函数参数为 **head)。
    • 需同时断开前驱和后继节点与当前节点的关联,避免链表断裂。

运行结果

plaintext

初始链表: 1 2 3 4 5 
找到节点: 3
修改后链表: 1 2 30 4 5 
插入后链表: 1 2 25 30 4 5 
删除后链表: 1 2 25 30 5 

通过以上代码,可以完整实现双链表的增删改查功能,且处理了边界情况(如空链表、头节点操作、尾节点操作等)。

3 环形数组

在 C 语言中,环形数组(Circular Array) 是一种特殊的数组结构,它通过逻辑上的 “首尾相连” 来模拟环形特性。

其核心思想是:当数组的尾指针到达物理末尾时,会绕回数组的起始位置,从而实现高效的循环利用空间。环形数组常用于实现队列(尤其是循环队列)、缓冲区等场景,相比普通数组,它能更高效地利用内存(避免 “假溢出”)。

一、环形数组的核心概念

环形数组的实现依赖两个关键指针(索引):

  • 头指针(front):指向数组中有效元素的起始位置。
  • 尾指针(rear):指向数组中有效元素的下一个位置(即下一个待插入元素的位置)。

为了区分 “数组为空” 和 “数组满” 的状态,通常有两种处理方式:

  1. 预留一个空位置(最常用):当 (rear + 1) % capacity == front 时,表示数组已满。
  2. 用计数器记录元素个数:通过额外变量 size 直接判断空(size == 0)或满(size == capacity)。

本文以 “预留空位置” 为例讲解。

二、环形数组的创建(初始化)

环形数组的创建需要定义:

  • 存储数据的数组(data)。
  • 容量(capacity):数组的最大元素个数(实际可用 capacity - 1 个,因预留一个空位置)。
  • 头指针(front)和尾指针(rear):初始值均为 0(表示数组为空)。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>// 环形数组结构体
typedef struct {int* data;       // 存储数据的数组int capacity;    // 容量(最大可存储 capacity-1 个元素)int front;       // 头指针(指向首元素)int rear;        // 尾指针(指向尾元素的下一个位置)
} CircularArray;// 初始化环形数组(创建)
CircularArray* createCircularArray(int capacity) {// 容量至少为1,预留一个空位置,实际可用 capacity-1 个if (capacity <= 1) {printf("容量必须大于1\n");return NULL;}CircularArray* arr = (CircularArray*)malloc(sizeof(CircularArray));arr->data = (int*)malloc(sizeof(int) * capacity);arr->capacity = capacity;arr->front = 0;   // 初始头指针在0arr->rear = 0;    // 初始尾指针在0(数组为空)return arr;
}

三、环形数组的核心操作(增删改查)

1. 判断空 / 满状态(辅助函数)
// 判断数组是否为空
bool isEmpty(CircularArray* arr) {return arr->front == arr->rear;
}// 判断数组是否已满
bool isFull(CircularArray* arr) {// 尾指针的下一个位置等于头指针时,数组满return (arr->rear + 1) % arr->capacity == arr->front;
}
2. 增加元素(插入,从尾部添加)

在环形数组中,插入操作通常从尾部(rear 位置)添加元素,然后移动 rear 指针(绕回处理)。

// 向环形数组添加元素(从尾部插入)
bool insert(CircularArray* arr, int val) {if (isFull(arr)) {printf("数组已满,无法插入\n");return false;}arr->data[arr->rear] = val;               // 在尾指针位置存入值arr->rear = (arr->rear + 1) % arr->capacity;  // 尾指针后移(绕回)return true;
}
3. 删除元素(从头部删除)

删除操作通常从头部(front 位置)移除元素,然后移动 front 指针(绕回处理)。

// 从环形数组删除元素(从头部删除)
bool delete(CircularArray* arr, int* val) {if (isEmpty(arr)) {printf("数组为空,无法删除\n");return false;}*val = arr->data[arr->front];             // 取出头指针位置的值arr->front = (arr->front + 1) % arr->capacity;  // 头指针后移(绕回)return true;
}
4. 修改元素(根据位置修改)

环形数组的 “位置” 是逻辑位置(从 front 开始计数),需先将逻辑位置转换为物理索引(数组下标)。

// 计算逻辑位置对应的物理索引
int getPhysicalIndex(CircularArray* arr, int logicPos) {int size = (arr->rear - arr->front + arr->capacity) % arr->capacity;if (logicPos < 0 || logicPos >= size) {return -1;  // 逻辑位置无效}return (arr->front + logicPos) % arr->capacity;
}// 修改指定逻辑位置的元素
bool update(CircularArray* arr, int logicPos, int newVal) {int physPos = getPhysicalIndex(arr, logicPos);if (physPos == -1) {printf("位置无效,无法修改\n");return false;}arr->data[physPos] = newVal;return true;
}
5. 查询元素(根据位置查询或查找值)
  • 按位置查询:将逻辑位置转换为物理索引后取值。
  • 按值查询:遍历有效元素,返回第一个匹配值的逻辑位置。
// 按逻辑位置查询元素
bool get(CircularArray* arr, int logicPos, int* val) {int physPos = getPhysicalIndex(arr, logicPos);if (physPos == -1) {printf("位置无效,无法查询\n");return false;}*val = arr->data[physPos];return true;
}// 查找值对应的逻辑位置(返回第一个匹配值的位置,未找到返回-1)
int find(CircularArray* arr, int target) {if (isEmpty(arr)) {return -1;}int size = (arr->rear - arr->front + arr->capacity) % arr->capacity;for (int i = 0; i < size; i++) {int physPos = (arr->front + i) % arr->capacity;if (arr->data[physPos] == target) {return i;  // 返回逻辑位置}}return -1;
}

四、辅助操作(打印、销毁)

// 打印环形数组的所有元素
void printCircularArray(CircularArray* arr) {if (isEmpty(arr)) {printf("数组为空\n");return;}printf("环形数组元素: ");int cur = arr->front;while (cur != arr->rear) {printf("%d ", arr->data[cur]);cur = (cur + 1) % arr->capacity;}printf("\n");
}// 销毁环形数组(释放内存)
void destroyCircularArray(CircularArray* arr) {if (arr != NULL) {free(arr->data);   // 释放数据数组free(arr);         // 释放结构体}
}

五、完整测试示例

int main() {// 创建容量为5的环形数组(实际可存4个元素)CircularArray* arr = createCircularArray(5);if (arr == NULL) return 1;// 插入元素insert(arr, 10);insert(arr, 20);insert(arr, 30);insert(arr, 40);printCircularArray(arr);  // 输出:10 20 30 40// 测试数组满的情况insert(arr, 50);  // 提示:数组已满,无法插入// 查询元素int val;get(arr, 2, &val);  // 查询逻辑位置2的元素printf("位置2的元素: %d\n", val);  // 输出:30// 修改元素(将逻辑位置1的元素改为200)update(arr, 1, 200);printCircularArray(arr);  // 输出:10 200 30 40// 查找元素int pos = find(arr, 30);printf("值30的位置: %d\n", pos);  // 输出:2// 删除元素(从头部删除)delete(arr, &val);printf("删除的元素: %d\n", val);  // 输出:10printCircularArray(arr);  // 输出:200 30 40// 再插入一个元素(测试绕回)insert(arr, 50);printCircularArray(arr);  // 输出:200 30 40 50// 销毁数组destroyCircularArray(arr);return 0;
}

六、环形数组的特点与适用场景

特点:
  • 空间高效:避免普通数组 “删除头部元素后,前面空间浪费” 的问题(通过指针绕回复用空间)。
  • 操作高效:增删操作时间复杂度为 O(1)(只需移动指针)。
  • 固定容量:创建时需指定容量,无法动态扩容(如需扩容需手动实现)。
适用场景:
  • 实现循环队列(如生产者 - 消费者模型的缓冲区)。
  • 需要循环遍历数据的场景(如轮询任务调度)。
  • 限制最大容量的缓存设计。

通过上述实现,环形数组的创建、增删改查操作已完整覆盖,核心在于理解 front 和 rear 指针的绕回逻辑,以及空 / 满状态的判断。

4 双端队列

双端队列(Double-Ended Queue,简称 Deque)是一种特殊的线性数据结构,它允许在队列的头部和尾部同时进行元素的插入(Add)和删除(Remove)操作,相当于融合了栈(只能尾操作)和普通队列(头删尾插)的功能。

一、双端队列的核心特性

  1. 双向操作:这是 Deque 最核心的特点
    • 头部(Front):支持插入元素(addFirst)和删除元素(removeFirst)。
    • 尾部(Rear):支持插入元素(addLast)和删除元素(removeLast)。
  2. 兼容栈与队列
    • 若只使用尾部的插入 / 删除(addLast/removeLast),Deque 就是一个(后进先出,LIFO)。
    • 若只使用尾部插入(addLast)和头部删除(removeFirst),Deque 就是一个普通队列(先进先出,FIFO)。
  3. 随机访问?不一定
    • 基于数组实现的 Deque(如 Java 的 ArrayDeque)支持 O (1) 随机访问(需计算真实索引);
    • 基于链表实现的 Deque(如 Java 的 LinkedList)随机访问需 O (N) 时间(需遍历)。

二、双端队列的常用操作(以 Java Deque 接口为例)

所有操作的时间复杂度,数组实现为 O (1),链表实现也为 O (1)(无需搬移元素,只需修改指针)。

操作类型头部操作(Front)尾部操作(Rear)说明
插入元素addFirst(E e) / offerFirst(E e)addLast(E e) / offerLast(E e)add 满时抛异常,offer 满时返回 false
删除元素removeFirst() / pollFirst()removeLast() / pollLast()remove 空时抛异常,poll 空时返回 null
获取元素(不删)getFirst() / peekFirst()getLast() / peekLast()get 空时抛异常,peek 空时返回 null
判断空 / 满isEmpty()size()无直接 isFull(),数组实现需自己判断容量

三、双端队列的两种实现方式对比

Deque 主要有 “数组实现” 和 “链表实现” 两种,各有优劣,适配不同场景。

实现方式代表(Java 标准库)优点缺点适用场景
环形数组ArrayDeque1. 随机访问快(O (1))2. 尾操作性能极高1. 容量固定(需动态扩容)2. 中间增删慢(O (N))高频头尾操作、需随机访问的场景
双向链表LinkedList1. 容量动态(无需扩容)2. 中间增删灵活1. 随机访问慢(O (N))2. 内存开销大(每个节点存前后指针)元素数量不确定、需中间操作的场景

四、双端队列的典型应用场景

  1. 实现栈或队列
    • 用 ArrayDeque 实现栈(比 Stack 类高效):push() = addLast()pop() = removeLast()
    • 用 ArrayDeque 实现队列(比 LinkedList 高效):offer() = addLast()poll() = removeFirst()
  2. 滑动窗口问题:算法题中常见(如 “滑动窗口最大值”),用 Deque 维护窗口内的元素,头部存窗口的 “候选最大值”,尾部处理新加入的元素,可将时间复杂度从 O (N²) 降至 O (N)。
  3. 浏览器前进 / 后退功能:用两个 Deque 分别存储 “历史记录” 和 “前进记录”,点击前进 / 后退时从对应队列取元素。
  4. 任务调度:如线程池的任务队列,支持从头部取紧急任务,从尾部加普通任务。

五、简单代码示例(Java ArrayDeque

import java.util.ArrayDeque;
import java.util.Deque;public class DequeDemo {public static void main(String[] args) {// 创建一个 ArrayDeque(基于环形数组的 Deque)Deque<Integer> deque = new ArrayDeque<>();// 尾部插入(普通队列操作)deque.addLast(10);deque.addLast(20);deque.addLast(30);System.out.println("初始队列:" + deque); // 输出:[10, 20, 30]// 头部插入(Deque 特有)deque.addFirst(5);System.out.println("头部插入5后:" + deque); // 输出:[5, 10, 20, 30]// 头部删除(普通队列操作)int first = deque.removeFirst();System.out.println("删除的头部元素:" + first); // 输出:5System.out.println("删除后队列:" + deque); // 输出:[10, 20, 30]// 尾部删除(栈操作)int last = deque.removeLast();System.out.println("删除的尾部元素:" + last); // 输出:30System.out.println("删除后队列:" + deque); // 输出:[10, 20]// 获取头部/尾部元素(不删除)System.out.println("当前头部元素:" + deque.getFirst()); // 输出:10System.out.println("当前尾部元素:" + deque.getLast()); // 输出:20}
}

总结

双端队列的本质是 “双向可操作的线性容器”,它既解决了普通队列 “只能尾插头删” 和栈 “只能尾操作” 的局限性,又通过不同的实现方式(环形数组 / 链表)适配了不同的性能需求。标准库选择用环形数组实现 ArrayDeque,正是因为它在 “头尾高频操作” 场景下,能最大化发挥 O (1) 性能的优势,同时避免了普通数组头部操作的 O (N) 损耗。

c语言实现:

用 C 语言实现双端队列(Deque)的完整示例,基于环形数组实现(兼顾效率和简洁性),包含核心的增删查操作,并附带测试代码。

一、双端队列的定义与实现

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>// 双端队列结构体(基于环形数组)
typedef struct {int* data;       // 存储元素的数组int capacity;    // 容量(最大可存储元素数)int front;       // 头指针(指向队头元素)int rear;        // 尾指针(指向队尾元素的下一个位置)int size;        // 当前元素个数(简化空/满判断)
} Deque;// 1. 初始化双端队列(创建)
Deque* dequeCreate(int capacity) {if (capacity <= 0) {printf("容量必须大于0\n");return NULL;}Deque* deque = (Deque*)malloc(sizeof(Deque));deque->data = (int*)malloc(sizeof(int) * capacity);deque->capacity = capacity;deque->front = 0;   // 初始头指针在0deque->rear = 0;    // 初始尾指针在0deque->size = 0;    // 初始元素数为0return deque;
}// 2. 判断队列是否为空
bool dequeIsEmpty(Deque* deque) {return deque->size == 0;
}// 3. 判断队列是否已满
bool dequeIsFull(Deque* deque) {return deque->size == deque->capacity;
}// 4. 头部插入元素(addFirst)
bool dequeAddFirst(Deque* deque, int val) {if (dequeIsFull(deque)) {printf("队列已满,无法头部插入\n");return false;}// 头指针向前移动(环形处理:若front为0,绕回至capacity-1)deque->front = (deque->front - 1 + deque->capacity) % deque->capacity;deque->data[deque->front] = val;  // 存入新元素deque->size++;return true;
}// 5. 尾部插入元素(addLast)
bool dequeAddLast(Deque* deque, int val) {if (dequeIsFull(deque)) {printf("队列已满,无法尾部插入\n");return false;}deque->data[deque->rear] = val;  // 尾指针位置存入新元素// 尾指针向后移动(环形处理)deque->rear = (deque->rear + 1) % deque->capacity;deque->size++;return true;
}// 6. 头部删除元素(removeFirst)
bool dequeRemoveFirst(Deque* deque, int* val) {if (dequeIsEmpty(deque)) {printf("队列为空,无法头部删除\n");return false;}*val = deque->data[deque->front];  // 取出头部元素// 头指针向后移动(环形处理)deque->front = (deque->front + 1) % deque->capacity;deque->size--;return true;
}// 7. 尾部删除元素(removeLast)
bool dequeRemoveLast(Deque* deque, int* val) {if (dequeIsEmpty(deque)) {printf("队列为空,无法尾部删除\n");return false;}// 尾指针向前移动(环形处理)deque->rear = (deque->rear - 1 + deque->capacity) % deque->capacity;*val = deque->data[deque->rear];  // 取出尾部元素deque->size--;return true;
}// 8. 获取头部元素(不删除,getFirst)
bool dequeGetFirst(Deque* deque, int* val) {if (dequeIsEmpty(deque)) {printf("队列为空,无法获取头部元素\n");return false;}*val = deque->data[deque->front];return true;
}// 9. 获取尾部元素(不删除,getLast)
bool dequeGetLast(Deque* deque, int* val) {if (dequeIsEmpty(deque)) {printf("队列为空,无法获取尾部元素\n");return false;}// 尾部元素在 rear 的前一个位置(环形处理)int lastIdx = (deque->rear - 1 + deque->capacity) % deque->capacity;*val = deque->data[lastIdx];return true;
}// 10. 打印队列元素(从头部到尾部)
void dequePrint(Deque* deque) {if (dequeIsEmpty(deque)) {printf("队列为空\n");return;}printf("双端队列元素: ");int cur = deque->front;for (int i = 0; i < deque->size; i++) {printf("%d ", deque->data[cur]);cur = (cur + 1) % deque->capacity;  // 环形遍历}printf("\n");
}// 11. 销毁队列(释放内存)
void dequeDestroy(Deque* deque) {if (deque != NULL) {free(deque->data);free(deque);}
}

二、测试代码

int main() {// 创建容量为5的双端队列Deque* deque = dequeCreate(5);if (deque == NULL) return 1;// 尾部插入元素(模拟普通队列)dequeAddLast(deque, 10);dequeAddLast(deque, 20);dequeAddLast(deque, 30);dequePrint(deque);  // 输出:双端队列元素: 10 20 30 // 头部插入元素(双端队列特有)dequeAddFirst(deque, 5);dequePrint(deque);  // 输出:双端队列元素: 5 10 20 30 // 再头部插入一个元素dequeAddFirst(deque, 1);dequePrint(deque);  // 输出:双端队列元素: 1 5 10 20 30 // 测试队列满的情况dequeAddFirst(deque, 0);  // 提示:队列已满,无法头部插入// 获取头部和尾部元素int val;dequeGetFirst(deque, &val);printf("头部元素: %d\n", val);  // 输出:1dequeGetLast(deque, &val);printf("尾部元素: %d\n", val);  // 输出:30// 头部删除元素(模拟普通队列)dequeRemoveFirst(deque, &val);printf("删除的头部元素: %d\n", val);  // 输出:1dequePrint(deque);  // 输出:双端队列元素: 5 10 20 30 // 尾部删除元素(模拟栈)dequeRemoveLast(deque, &val);printf("删除的尾部元素: %d\n", val);  // 输出:30dequePrint(deque);  // 输出:双端队列元素: 5 10 20 // 销毁队列dequeDestroy(deque);return 0;
}

三、代码说明

  1. 核心设计:用环形数组实现,通过 front(头指针)、rear(尾指针)和 size(元素个数)管理队列。size 简化了空 / 满判断(避免了复杂的取模逻辑)。

  2. 关键操作

    • 头部插入(dequeAddFirst:头指针先向前移动(绕回处理),再存入元素。
    • 尾部插入(dequeAddLast:尾指针位置存入元素,再向后移动(绕回处理)。
    • 头部删除(dequeRemoveFirst:取出头部元素,头指针向后移动(绕回处理)。
    • 尾部删除(dequeRemoveLast:尾指针先向前移动(绕回处理),再取出元素。
  3. 时间复杂度:所有操作(增删查)均为 O(1),因为仅涉及指针移动和取模运算,无需搬移元素。

  4. 适用场景:适合需要高频头部 / 尾部操作的场景(如实现栈、队列、滑动窗口等),性能优于链表实现的双端队列。

运行结果

plaintext

双端队列元素: 10 20 30 
双端队列元素: 5 10 20 30 
双端队列元素: 1 5 10 20 30 
队列已满,无法头部插入
头部元素: 1
尾部元素: 30
删除的头部元素: 1
双端队列元素: 5 10 20 30 
删除的尾部元素: 30
双端队列元素: 5 10 20 

这个示例完整展示了双端队列的双向操作能力,你可以根据需要扩展功能(如动态扩容、支持泛型等)。

5 为什么编程语言标准库的动态数组(如 Java 的 ArrayList、C++ 的 vector、Python 的 list)底层不使用环形数组?

一、先明确核心前提:环形数组的「O (1) 头尾操作」有严格限制

环形数组确实能实现 O (1) 时间复杂度的头部 / 尾部增删,但这依赖一个关键条件:容量固定。环形数组的核心是通过 front(头指针)和 rear(尾指针)的 “绕回” 逻辑复用空间,而指针的计算依赖 固定的容量大小capacity)。如果要支持动态扩容(这是标准库动态数组的核心特性),环形数组的 “绕回” 逻辑会被打破:

  • 当数组满时,需要申请新的更大容量的内存,并将原有元素复制到新空间。此时,front 和 rear 的相对位置需要重新计算,本质上和普通数组的扩容操作完全一致(没有优势)。
  • 扩容后,原有的 “环形” 特性在新数组中暂时失效(因为新数组有大量空闲空间,指针无需绕回),直到元素再次填满新数组。

二、动态数组的核心需求:随机访问优先,且需动态扩容

标准库的动态数组(如 ArrayList)的设计目标是 “提供类似数组的随机访问能力,同时支持动态增减元素”。其核心需求包括:

  1. O (1) 随机访问:通过索引直接定位元素(arr[i]),这是数组最核心的优势。
  2. 动态扩容:无需预先指定容量,元素数量可灵活增长(通常按 1.5 倍或 2 倍扩容)。
  3. 尽可能高效的增删:尤其是尾部操作(日常使用最频繁)需 O (1) 时间,中间 / 头部操作允许 O (N) 时间(因为场景较少)。

环形数组虽然能满足 “随机访问”(通过计算真实索引)和 “头尾 O (1) 操作”,但在 “动态扩容” 和 “工程实现复杂度” 上存在难以解决的问题。

三、环形数组不被采用的具体原因

1. 动态扩容时,环形特性失去意义,且实现更复杂

普通动态数组扩容时,直接将元素按顺序复制到新数组即可,逻辑简单:

// 普通数组扩容(简化)
new_data[i] = old_data[i];  // 直接按索引复制

而环形数组扩容时,由于元素在物理空间上可能是 “断裂” 的(front 在后,rear 在前),复制逻辑必须分两段处理:

// 环形数组扩容(简化)
int idx = front;
int i = 0;
// 第一段:从 front 到旧数组末尾
while (idx < capacity) {new_data[i++] = old_data[idx++];
}
// 第二段:从旧数组开头到 rear-1
idx = 0;
while (idx < rear) {new_data[i++] = old_data[idx++];
}

这种分段复制不仅 没有性能优势(同样是 O (N) 时间),还增加了代码复杂度(需要处理边界条件)。

更重要的是:扩容后的新数组有大量空闲空间,front 会被重置为 0,rear 指向元素末尾,此时环形数组退化为普通数组(直到再次填满)。也就是说,环形数组的 “环形” 特性在扩容后会暂时失效,其优势仅在 “接近满容量” 时体现,但动态数组的日常使用中 “接近满容量” 是少数情况。

2. 随机访问的实现更复杂,且存在隐性成本

普通数组的随机访问直接通过索引计算地址(硬件级支持):

// 普通数组访问:直接映射
value = data[i];  // 地址 = data + i * sizeof(int)

环形数组的随机访问需要先计算 “真实索引”:

// 环形数组访问:需额外计算
int size = (rear - front + capacity) % capacity;  // 先算当前元素个数
if (i < 0 || i >= size) { ... }  // 检查索引有效性
int real_idx = (front + i) % capacity;  // 计算真实索引
value = data[real_idx];

虽然计算本身是 O (1) 时间,但相比普通数组多了 两次取模运算 和 一次边界检查取模运算在硬件层面比直接加法更耗时)。对于高频的随机访问操作(动态数组的核心场景),这种隐性成本会被放大。

3. 中间元素的增删操作无优势,且逻辑更复杂

环形数组对 中间元素的增删(如 add(index, val)remove(index))和普通数组一样需要搬移元素,时间复杂度都是 O (N),但实现更复杂:

  • 普通数组搬移时,直接从索引 i 开始整体移动(data[j+1] = data[j])。
  • 环形数组搬移时,需要先判断索引在 front 的左侧还是右侧,再决定搬移方向(向左或向右),并处理可能的 “绕回” 情况(例如,搬移到数组末尾时需要绕回开头)。

这种复杂性会导致代码更难维护,且容易引入 bugs(尤其是边界条件处理)。

4. 内存空间的 “碎片化” 感知问题

环形数组的 “环形” 是逻辑上的,其物理内存依然是连续的(和普通数组一样)。但当 front 不为 0 时,数组头部会有一段空闲空间([0, front-1]),这段空间在逻辑上属于 “已使用”(被环形复用),但在物理上是空闲的。

这种 “物理空闲但逻辑占用” 的状态会给开发者带来困扰:例如,调试时查看数组的原始内存会看到不连续的元素(前半部分空闲,后半部分有值,中间被 front 分割),不如普通数组的 “元素连续存储” 直观。标准库需要兼顾易用性和可调试性,环形数组的这种 “不直观” 是减分项。

5. 与 “动态数组” 的设计定位冲突

动态数组的核心是 “以数组为基础,提供动态能力”,其设计目标是 “尽可能保留数组的简单性和高效性”。环形数组的 “环形” 特性本质上是对数组的 “复杂化改造”,但这种改造带来的收益(头尾 O (1) 操作)在动态数组的使用场景中并不突出:

  • 头部增删(addFirstremoveFirst)在实际开发中远不如尾部操作(addLastremoveLast)频繁。
  • 如果需要高频的头尾操作,开发者会选择链表(如 Java 的 LinkedList)或双端队列(如 ArrayDeque),而非动态数组。

标准库通常会提供多种数据结构(数组、链表、队列)以满足不同场景,动态数组的定位是 “优化随机访问和尾部操作”,而非 “兼顾所有场景”。环形数组的 “全能” 反而使其失去了动态数组的核心定位。

四、反例:为什么 ArrayDeque 用了环形数组?

Java 的 ArrayDeque(双端队列)底层使用了环形数组,但它 不是动态数组,而是专门优化头尾操作的容器。其设计目标与动态数组完全不同:

  • ArrayDeque 不支持随机访问(没有 get(index) 方法),避免了环形数组随机访问的复杂性。
  • 它的核心场景是头尾增删(addFirstaddLast 等),刚好匹配环形数组的优势。
  • 虽然支持动态扩容,但扩容逻辑是为了 “双端队列” 的场景设计的,而非 “随机访问”。

这恰恰说明:环形数组适合特定场景(如双端队列),但不适合动态数组的核心场景(随机访问 + 动态扩容)。

总结

编程语言标准库的动态数组不使用环形数组,核心原因是 “收益有限,成本过高”

  • 收益:仅在 “容量固定且接近满” 时,头尾操作能保持 O (1) 时间,但动态数组的扩容特性会让这种收益大部分时间失效。
  • 成本:随机访问更复杂、扩容逻辑更繁琐、中间操作无优势、代码维护难度高。

动态数组的设计哲学是 “以最简单的方式满足核心需求”(随机访问 + 动态扩容),而环形数组的 “环形” 特性与这一哲学冲突。因此,标准库选择了更简单、更符合核心场景的普通数组作为底层实现。

东哥的理解:

如果用环形数组,增删查改的的所有操作都会涉及 % 求模运算,这个操作是比较消耗性能的。

尤其像数组的 get 方法,调用频率会非常非常高,如果每次调用都多一步 % 运算,加起来的性能损耗远大于环形数组带来的收益,因为数组很少在头部增删元素。

如果你非要在头部增删,应该使用更合适的其他数据结构。 所以一般只会在双端队列这种场景下使用环形数组,标准的动态数组并没有使用这个技巧。

不是不能用,而是算总账不划算。

-------

环形数组的 “绕回” 优势,抵不过其日常高频操作(如 get)的性能损耗和场景错位

http://www.dtcms.com/a/504416.html

相关文章:

  • Annual Comedy Competition (Season 2)
  • 建设部网站办事大厅成都plc培训机构哪家最好
  • 在住房和城乡建设部网站查询在线设计平台的缺点
  • 公司网页建立乐陵seo优化信德
  • 做静态网站有什么建议wordpress文章没缩略图
  • 做网站提成仿做网站的网站
  • ICT 数字测试原理 31 - - X-Tree测试
  • 网站服务器ip地址在哪里看网站内容运营方案
  • CSDN-Markdown新版说明
  • 汕头网站推广seo在线咨询平台系统
  • 设计教程网站推荐网站开发 质量管理
  • 阆中网站网站建设郴州网站建设公司哪个好
  • 【STM32笔记】:P04 断言的使用
  • 阿里巴巴建网站龙华网站建设专业公司
  • 新手学wordpress优化落实防控措施
  • 基于彩色线图像增强和改进型YOLOv7模型的海洋水产养殖生物体检测
  • 网站可以免费建立吗网络游戏企业不得向提供游戏服务
  • codetop高频(3)
  • 兰州网络推广关键词优化网站营销seo
  • 开发大型网站的流程wordpress自定义搜索
  • 站长 网站ip怒江商城网站建设
  • 商丘网站建设专业现状wordpress网页版入口
  • QDarkStyleSheet: 一个Qt应用的暗色主题解决方案
  • 各种网站app长沙人才市场招聘
  • 从零开始的C++学习生活 10:继承和多态
  • 记事本怎么做网站一人办厂千元投资
  • Java代码之gradle(1)
  • 卖印花图案设计网站网站建设实验小结
  • 织梦网站如何做seowordpress类
  • C# 数据加载专题 之泛型序列化