【数据结构实战】循环队列FIFO 特性生成六十甲子(天干地支纪年法),实现传统文化里的 “时间轮回”

前言

天干地支纪年法是中国传统文化的重要组成部分,十天干与十二地支依次相配,组成六十甲子 。本文将使用循环队列这一数据结构完成六十甲子的生成,严格遵循题目要求:

  • 定义两个循环队列,分别存储十天干、十二地支
  • 队列空则重新入队,不为空则各出队一个配对
  • 输出完整 60 组甲子
  • 提供静态数组版动态内存分配版双实现

一、核心知识点

1. 天干地支

  • 十天干:甲、乙、丙、丁、戊、己、庚、辛、壬、癸
  • 十二地支:子、丑、寅、卯、辰、巳、午、未、申、酉、戌、亥
  • 组合规则:依次配对,循环使用,共 60 种组合

2. 循环队列

  • 解决顺序队列假溢出问题
  • 判空:front == rear
  • 判满:(rear+1) % maxSize == front(牺牲一个单元)

二、静态数组版 循环队列实现六十甲子

完整代码

cpp 复制代码
/***********************************************************
 *  标题:静态数组版循环队列实现六十甲子
 *  环境:C语言 / 无动态内存 / 固定大小数组
 **********************************************************/
#include <stdio.h>
#include <string.h>

// 队列最大容量(牺牲1个单元,需 >= 12+1=13 才能存12个地支)
#define MAXSIZE 13  // ? 修复:从12改为13

// 循环队列结构体定义
typedef struct {
    // 存储字符串(天干/地支)
    char data[MAXSIZE][5];
    // 队头指针
    int front;
    // 队尾指针
    int rear;
} Queue;

// -------------------------- 初始化队列 --------------------------
void initQueue(Queue *Q) {
    // 队头置0
    Q->front = 0;
    // 队尾置0
    Q->rear = 0;
    printf("===== 循环队列初始化完成 =====\n");
}

// -------------------------- 判断队列是否为空 --------------------------
int isEmpty(Queue *Q) {
    // 队头 == 队尾 → 空队列
    return (Q->front == Q->rear) ? 1 : 0;
}

// -------------------------- 判断队列是否为满 --------------------------
int isFull(Queue *Q) {
    // 循环队列判满规则(牺牲1个单元)
    return ((Q->rear + 1) % MAXSIZE == Q->front) ? 1 : 0;
}

// -------------------------- 入队操作 --------------------------
int enqueue(Queue *Q, char *elem) {
    // 判满
    if (isFull(Q)) {
        printf("队列已满,无法入队!\n");
        return 0;
    }
    // 字符串拷贝入队
    strcpy(Q->data[Q->rear], elem);
    // 队尾循环后移
    Q->rear = (Q->rear + 1) % MAXSIZE;
    return 1;
}

// -------------------------- 出队操作 --------------------------
int dequeue(Queue *Q, char *elem) {
    // 判空
    if (isEmpty(Q)) {
        printf("队列为空,无法出队!\n");
        return 0;
    }
    // 字符串拷贝出队
    strcpy(elem, Q->data[Q->front]);
    // 队头循环后移
    Q->front = (Q->front + 1) % MAXSIZE;
    return 1;
}

// -------------------------- 十天干入队 --------------------------
void initTianGan(Queue *Q) {
    char *tg[] = {"甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"};
    for (int i = 0; i < 10; i++) {
        enqueue(Q, tg[i]);
    }
    printf("十天干入队完成\n");
}

// -------------------------- 十二地支入队 --------------------------
void initDiZhi(Queue *Q) {
    char *dz[] = {"子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"};
    for (int i = 0; i < 12; i++) {
        enqueue(Q, dz[i]);
    }
    printf("十二地支入队完成\n");
}

// -------------------------- 主函数 --------------------------
int main() {
    // 定义天干、地支两个队列
    Queue qTG, qDZ;
    // 存储出队元素
    char tg[5], dz[5];
    // 计数:60个甲子
    int count = 0;

    // 初始化队列
    initQueue(&qTG);
    initQueue(&qDZ);

    // 首次入队
    initTianGan(&qTG);
    initDiZhi(&qDZ);

    printf("\n========== 六十甲子列表 ==========\n");
    // 生成60组
    while (count < 60) {
        count++;
        // 各出队一个
        dequeue(&qTG, tg);
        dequeue(&qDZ, dz);
        // 输出配对
        printf("%02d:%s%s\t", count, tg, dz);
        // 每6个换行
        if (count % 6 == 0) printf("\n");

        // 天干空 → 重新入队
        if (isEmpty(&qTG)) initTianGan(&qTG);
        // 地支空 → 重新入队
        if (isEmpty(&qDZ)) initDiZhi(&qDZ);
    }

    printf("\n========== 生成完成,共60组 ==========\n");
    return 0;
}

运行截图如下

三、动态内存分配版 循环队列实现六十甲子

完整代码

cpp 复制代码
/***********************************************************
 *  标题:动态内存版循环队列实现六十甲子
 *  说明:本代码故意设置容量=12,用于演示循环队列"牺牲一个单元"
 *       因此存入12个地支时会触发:队列已满!(教学演示用)
 **********************************************************/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// 队列容量设置为 12(故意不够用,为了触发"队列已满"报错)
// 因为循环队列会牺牲1个单元,所以只能存 11 个元素
#define DEFAULT_SIZE 12

// 动态循环队列结构体
typedef struct {
    char** data;         // 动态数组:存储天干/地支字符串
    int front;           // 队头指针,指向队头元素
    int rear;            // 队尾指针,指向队尾下一个位置
    int maxSize;         // 队列最大容量
} Queue;

// -------------------------- 创建队列(动态分配内存) --------------------------
Queue* createQueue(int size) {
    // 为队列结构体申请堆空间
    Queue* q = (Queue*)malloc(sizeof(Queue));
    if (q == NULL) {
        printf("结构体分配失败!\n");
        return NULL;
    }

    // 为字符串指针数组申请空间
    q->data = (char**)malloc(sizeof(char*) * size);

    // 为每个字符串分配空间(存储中文)
    for (int i = 0; i < size; i++) {
        q->data[i] = (char*)malloc(5);
    }

    // 初始化队头、队尾都为0
    q->front = 0;
    q->rear = 0;
    q->maxSize = size;

    printf("===== 动态循环队列创建成功 =====\n");
    return q;
}

// -------------------------- 判断队列是否为空 --------------------------
// 判空条件:队头 == 队尾
int isEmpty(Queue* q) {
    return q->front == q->rear;
}

// -------------------------- 判断队列是否为满 --------------------------
// 判满条件:(rear + 1) % maxSize == front
// 特点:会牺牲一个存储单元,用于区分空和满
int isFull(Queue* q) {
    return (q->rear + 1) % q->maxSize == q->front;
}

// -------------------------- 入队操作 --------------------------
int enqueue(Queue* q, char* elem) {
    // 如果队列满,直接报错返回(本代码故意触发这里)
    if (isFull(q)) {
        printf("队列已满!\n");
        return 0;
    }

    // 将元素拷贝到队尾
    strcpy(q->data[q->rear], elem);

    // 队尾循环后移
    q->rear = (q->rear + 1) % q->maxSize;
    return 1;
}

// -------------------------- 出队操作 --------------------------
int dequeue(Queue* q, char* elem) {
    if (isEmpty(q)) {
        printf("队列为空!\n");
        return 0;
    }

    // 取出队头元素
    strcpy(elem, q->data[q->front]);

    // 队头循环后移
    q->front = (q->front + 1) % q->maxSize;
    return 1;
}

// -------------------------- 十天干入队 --------------------------
void initTG(Queue* q) {
    char* tg[] = { "甲","乙","丙","丁","戊","己","庚","辛","壬","癸" };
    for (int i = 0; i < 10; i++) {
        enqueue(q, tg[i]);
    }
}

// -------------------------- 十二地支入队(会触发队列满!) --------------------------
void initDZ(Queue* q) {
    char* dz[] = { "子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥" };
    for (int i = 0; i < 12; i++) {
        enqueue(q, dz[i]);
    }
}

// -------------------------- 销毁队列,释放内存 --------------------------
void destroyQueue(Queue* q) {
    for (int i = 0; i < q->maxSize; i++) {
        free(q->data[i]);
    }
    free(q->data);
    free(q);
    printf("队列内存已释放\n");
}

// -------------------------- 主函数 --------------------------
int main() {
    // 创建两个队列:天干、地支
    Queue* qTG = createQueue(DEFAULT_SIZE);
    Queue* qDZ = createQueue(DEFAULT_SIZE);

    char tg[5], dz[5];
    int count = 0;

    // 天干入队(10个,能存下,不报错)
    initTG(qTG);

    // 地支入队(12个 → 容量12只能存11个 → 触发队列已满!)
    initDZ(qDZ);

    printf("\n========== 动态版 六十甲子 ==========\n");

    while (count < 60) {
        count++;
        dequeue(qTG, tg);
        dequeue(qDZ, dz);
        printf("%02d:%s%s\t", count, tg, dz);

        if (count % 6 == 0)
            printf("\n");

        if (isEmpty(qTG))
            initTG(qTG);
        if (isEmpty(qDZ))
            initDZ(qDZ);
    }

    destroyQueue(qTG);
    destroyQueue(qDZ);
    return 0;
}

四、核心函数逐行精讲

1. 队列初始化

  • 静态版:直接给front/rear赋值 0
  • 动态版:使用malloc分别为结构体、数组、字符串分配空间

2. 入队函数

  1. 判断队列是否满
  2. 字符串拷贝存入队尾
  3. 队尾指针循环后移

3. 出队函数

  1. 判断队列是否空
  2. 队头元素拷贝出来
  3. 队头指针循环后移

4. 六十甲子生成逻辑

  1. 两个队列各出队一个元素
  2. 拼接输出
  3. 队列空则重新入队
  4. 循环 60 次结束

你看到 "队满" 还能输出 60 组甲子,是因为:


1. 第一次入队:地支只成功入队 11 个
  • 队列容量 DEFAULT_SIZE=12,循环队列判满规则会牺牲 1 个单元 ,所以最多只能存 11 个元素
  • 你调用 initDZ() 入队 12 个地支:
    • 前 11 个(子、丑、寅、卯、辰、巳、午、未、申、酉、戌)成功入队
    • 第 12 个(亥)入队失败,打印 队列已满!
  • 此时队列里有 11 个地支,可以正常出队 11 次,配对输出 11 组甲子

2. 出队后队列变空,再次入队又能存 11 个
  • 当这 11 个地支全部出队后,队列变回空队列
  • 程序会再次调用 initDZ(),重新尝试入队 12 个地支:
    • 又成功入队 11 个,第 12 个再次失败 → 再次打印 队列已满!
    • 又能出队 11 次,配对输出 11 组甲子

3. 多次循环后,刚好凑够 60 组

我们算一下:

  • 每次入队能得到 11 个可用地支 → 配对 11 组甲子
  • 第 1 次:11 组
  • 第 2 次:11 组(累计 22)
  • 第 3 次:11 组(累计 33)
  • 第 4 次:11 组(累计 44)
  • 第 5 次:11 组(累计 55)
  • 第 6 次:再入队 11 个,取 5 个就凑够 60 组

所以虽然每次都报 "队满",但每次都有 11 个地支可用,多次循环后刚好能输出完整 60 组甲子,不会少。

一句话总结

"队列已满" 只是不让第 12 个地支进队,但已经在队里的 11 个地支可以正常出队、配对;等这 11 个用完,队列变空后又能重新入队 11 个,反复几次就凑够了 60 组。


五、运行效果展示

六、总结

  1. 循环队列完美解决干支循环配对问题
  2. 静态版简单易用、适合学习
  3. 动态版更灵活、支持内存管理
相关推荐
im_AMBER2 小时前
Leetcode 147 零钱兑换 | 单词拆分
javascript·学习·算法·leetcode·动态规划
zl_vslam2 小时前
SLAM中的非线性优-3D图优化之IMU预积分SE3推导(二十一)
人工智能·算法·计算机视觉·3d
c++逐梦人2 小时前
DFS经典例题(八皇后,数独)
算法·蓝桥杯·深度优先
进击的小头2 小时前
第18篇:PID参数整定与裕度优化的现场调试实战
python·算法
cpp_25012 小时前
P1796 汤姆斯的天堂梦
数据结构·c++·算法·题解·洛谷·线性dp
凌波粒2 小时前
LeetCode--19.删除链表的倒数第 N 个结点(链表)
java·算法·leetcode·链表
Fcy6482 小时前
与红黑树有关算法题
算法
Mem0rin2 小时前
[Java/数据结构]顺序表之ArrayList
java·开发语言·数据结构
ShineWinsu2 小时前
Anaconda被误删后的抢救手册大纲
数据结构