算法学习(2)-栈、队列、链表

目录

队列-解密QQ号

栈-回文解密

纸牌游戏-小猫钓鱼

链表

模拟链表


队列-解密QQ号

        新学期开始了,小哈是小哼的新同桌(小哈是个小美女哦~),小哼向小哈询问 QQ 号, 小哈当然不会直接告诉小哼啦,原因嘛你懂的。

        所以小哈给了小哼一串加密过的数字,同时 小哈也告诉了小哼解密规则。

        规则是这样的:首先将第 1 个数删除,紧接着将第 2 个数放到 这串数的末尾,再将第 3 个数删除并将第 4 个数放到这串数的末尾,再将第 5 个数删除…… 直到剩下最后一个数,将最后一个数也删除。按照刚才删除的顺序,把这些删除的数连在一 起就是小哈的 QQ 啦。小哈给小哼加密过的一串数是“6 3 1 7 5 8 9 2 4”。

        代码实现如下:

#include <stdio.h>
int main() {
    int q[102] = {0, 6, 3, 1, 7, 5, 8, 9, 2, 4}, head, tail;
    int i;
    //初始化队列
    head = 1;
    tail = 10;
    while(head < tail) {
        //打印队首并将队首出队
        printf("%d ", q[head]);
        head++;
        //先将新队首的数添加到队尾
        q[tail] = q[head];
        tail++;
        //再将队首出队
        head++;
    }
    getcahr();getcahr();
    retrun 0;
}

        最后的输出就是 6 1 5 9 4 7 2 8 3,现在我们再来总结一下队列的概念。

        队列是一种特 殊的线性结构,它只允许在队列的首部(head)进行删除操作,这称为“出队”,而在队列 的尾部(tail)进行插入操作,这称为“入队”。当队列中没有元素时,即(head==tail),称为空队列。

        在我们的日常生活中有很多情况都符合队列的特性。比如买票, 每个排队买票的窗口就是一个队列。在这个队列当中,新来的人总是站在队列的最后面,来得越早的人越靠前,也就越早能买到票,就是先来的人先服务,我们称为“先进先出”(First In First Out,FIFO)原则。

        队列将是我们今后学习广度优先搜索以及队列优化的 Bellman-Ford 最短路算法的核心 数据结构。所以现在将队列的三个基本元素(一个数组,两个变量)封装为一个结构体类型, 如下。

struct queue {
    int data[100];//队列的主体,用来存储数据
    int head;//队首
    int tail;//队尾
};

         上面定义了一个结构体类型,我们通常将其放在 main 函数的外面,请注意结构体的定义末尾有个;号。struct 是结构体的关键字,queue 是我们为这个结构体起的名字。这个结构体有三个成员分别是:整型数组 data、整型 head 和整型 tail。这样我们就可以把这三个部分放在一起作为一个整体来对待。你可以这么理解:我们定义了一个新的数据类型,这个新类型非常强大,用这个新类型定义出的每一个变量可以同时存储一个整型数组和两个整数。

        有了新的结构体类型,如何定义结构体变量呢?很简单,这与我们之前定义变量的方式是一样的,具体做法如下。

struct queue q;

         请注意 struct queue 需要整体使用,不能直接写 queue q;。这样我们就定义了一个结构体变量 q。这个结构体变量就可以满足队列的所有操作了。那又该如何访问结构体变量的内部成员呢?可以使用.号,它叫做成员运算符或者点号运算符,如下:

q.head=1; 
q.tail=1; 
scanf("%d",&q.data[q.tail]); 

         好了,下面这段代码就是使用结构体来实现的队列操作。

#include <stdio.h>

struct queue 
{ 
    int data[100]; // 队列的主体,用来存储内容
    int head;      // 队首
    int tail;      // 队尾
}; 

int main() 
{ 
    struct queue q; 
    int i; 
    // 初始化队列
    q.head = 1; 
    q.tail = 1; 
    for(i = 1; i <= 9; i++) 
    { 
        // 依次向队列插入9个数
        scanf("%d", &q.data[q.tail]); 
        q.tail++; 
    } 
    while(q.head < q.tail) // 当队列不为空的时候执行循环
    { 
        // 打印队首并将队首出队
        printf("%d ", q.data[q.head]); 
        q.head++; 
        // 先将新队首的数添加到队尾
        q.data[q.tail] = q.data[q.head]; 
        q.tail++; 
        // 再将队首出队
        q.head++; 
    } 
    getchar(); 
    getchar(); 
    return 0; 
} 

栈-回文解密

        上一节中我们学习了队列,它是一种先进先出的数据结构。还有一种是后进先出的数据 结构,它叫做栈。栈限定为只能在一端进行插入和删除操作。比如说有一个小桶,小桶的直径只能放一个小球,我们现在小桶内依次放入 2、1、3 号小球。假如你现在需要拿出 2 号小球, 那就必须先将 3 号小球拿出,再拿出 1 号小球,最后才能将 2 号小球拿出来。在刚才取小球的过程中,我们最先放进去的小球最后才能拿出来,最后放进去的小球却可以最先拿出来。

        生活中也有很多这样的例子,比如我们在吃桶装薯片的时候,要想吃掉最后一片,就必须把前面的全部吃完;再比如浏览网页的时候需要退回到之前的某个网页,我们需要一步步地点击后退键。 还有手枪的弹夹,在装子弹的时候,最后装入的那发子弹,是被第一个打出去的。

        栈的实现 也很简单,只需要一个一维数组和一个指向栈顶的变量 top 就可以了。我们通过 top 来对栈 进行插入和删除操作。

        栈究竟有哪些作用呢?我们来看一个例子。“xyzyx”是一个回文字符串,所谓回文字符 串就是指正读反读均相同的字符序列,如“席主席”、“记书记”、“aha”和“ahaha”均是回文,但“ahah”不是回文。通过栈这个数据结构我们将很容易判断一个字符串是否为回文。

        首先我们需要读取这行字符串,并求出这个字符串的长度。

char a[101]; 
int len; 
gets(a); 
len=strlen(a); 

        如果一个字符串是回文的话,那么它必须是中间对称的,我们需要求中点,即:

mid=len/2-1; 

        接下来就轮到栈出场了。 我们先将 mid 之前的字符全部入栈。因为这里的栈是用来存储字符的,所以这里用来实 现栈的数组类型是字符数组即 char s[101];,初始化栈很简单,top=0;就可以了。入栈的操作 是top++; s[top]=x; (假设需要入栈的字符暂存在字符变量x中),其实可以简写为s[++top]=x;。

for (i = 0; i <= mid; i++) { 
    s[++top]=a[i]; 
} 

        接下来进入判断回文的关键步骤。将当前栈中的字符依次出栈,看看是否能与 mid 之后 的字符一一匹配,如果都能匹配则说明这个字符串是回文字符串,否则这个字符串就不是回 文字符串。

for (i = mid + 1; i <= len - 1; i++) { 
    if (a[i] != s[top]) { 
        break; 
    } 
    top--; 
} 

if (top == 0) {
    printf("YES"); 
}
else {
    printf("NO"); 
}

        最后如果 top 的值为 0,就说明栈内所有的字符都被一一匹配了,那么这个字符串就是 回文字符串。完整的代码如下。

#include <stdio.h>
#include <string.h>

int main() 
{
    char a[101], s[101];
    int i, len, mid, next, top;

    gets(a); // 读入一行字符串
    len = strlen(a); // 求字符串的长度
    mid = len / 2 - 1; // 求字符串的中点

    top = 0; // 栈的初始化
    // 将mid前的字符依次入栈
    for (i = 0; i <= mid; i++) 
    {
        s[++top] = a[i];
    }

    // 判断字符串的长度是奇数还是偶数,并找出需要进行字符匹配的起始下标
    if (len % 2 == 0)
    {
        next = mid + 1;
    }
    else
    {
        next = mid + 2;
    }

    // 开始匹配
    for (i = next; i <= len - 1; i++) 
    { 
        if (a[i] != s[top]) 
        {
            break;
        }
        top--;
    }

    // 如果top的值为0,则说明栈内所有的字符都被一一匹配了
    if (top == 0) 
    {
        printf("YES");
    }
    else 
    {
        printf("NO");
    }

    getchar();
    getchar();
    return 0; 
}

        堆栈最早由 Alan M. Turing(艾伦·图灵)于 1946 年提出,当时是为了解决子程序的调用和返回。艾伦·图灵是个牛人,图灵奖就是以他的名字命名的。如果你对他感兴趣不妨去读一读《艾伦·图灵传:如谜的解谜者》和《图灵的秘密》。


纸牌游戏-小猫钓鱼

        星期天小哼和小哈约在一起玩桌游,他们正在玩一个非常古怪的扑克游戏——“小猫钓鱼”。

        游戏的规则是这样的:将一副扑克牌平均分成两份,每人拿一份。小哼先拿出手中的第一张扑克牌放在桌上,然后小哈也拿出手中的第一张扑克牌,并放在小哼刚打出的扑克牌的上面,就像这样两人交替出牌。出牌时,如果某人打出的牌与桌上某张牌的牌面相同,即可将两张相同的牌及其中间所夹的牌全部取走,并依次放到自己手中牌的末尾。当任意一人 手中的牌全部出完时,游戏结束,对手获胜。

        假如游戏开始时,小哼手中有 6 张牌,顺序为 2 4 1 2 5 6,小哈手中也有 6 张牌,顺序 为 3 1 3 5 6 4,最终谁会获胜呢?        

        这里我们做一个约定,小哼和小哈手中牌的牌面只有 1~9。 我们先来分析一下这个游戏有哪几种操作。小哼有两种操作,分别是出牌和赢牌。这恰好对应队列的两个操作,出牌就是出队,赢牌就是入队。小哈的操作和小哼是一样的。而桌子就是一个栈,每打出一张牌放到桌上就相当于入栈。当有人赢牌的时候,依次将牌从桌上拿走,这就相当于出栈。

        那如何解决赢牌的问题呢?赢牌的规则是:如果某人打出的牌与桌 上的某张牌相同,即可将两张牌以及中间所夹的牌全部取走。那如何知道桌上已经有哪些牌了呢?最简单的方法就是枚举桌上的每一张牌,当然也有更好的办法我们待会再说。OK, 总之,我们需要两个队列、一个栈来模拟整个游戏。

        这里我们不做推理了,直接给出代码:

#include <stdio.h>

struct queue
{
    int data[10000];
    int head;
    int tail;
};

struct stack
{
    int data[10];
    int top;
};

int main()
{
    struct queue q1, q2;
    struct stack s;
    int book[10];
    int i, t;

    // 初始化队列
    q1.head = 1;
    q1.tail = 1;
    q2.head = 1;
    q2.tail = 1;
    // 初始化栈
    s.top = 0;
    // 初始化用来标记的数组,用来标记哪些牌已经在桌上
    for (i = 1; i <= 9; i++) {
        book[i] = 0;
    }
        
    // 依次向队列插入6个数
    // 小哼手上的6张牌
    for (i = 1; i <= 6; i++)
    {
        scanf("%d", &q1.data[q1.tail]);
        q1.tail++;
    }
    // 小哈手上的6张牌
    for (i = 1; i <= 6; i++)
    {
        scanf("%d", &q2.data[q2.tail]);
        q2.tail++;
    }
    while (q1.head < q1.tail && q2.head < q2.tail) // 当队列不为空的时候执行循环
    {
        t = q1.data[q1.head]; // 小哼出一张牌
        // 判断小哼当前打出的牌是否能赢牌
        if (book[t] == 0) // 表明桌上没有牌面为t的牌
        {
            // 小哼此轮没有赢牌
            q1.head++; // 小哼已经打出一张牌,所以要把打出的牌出队
            s.top++;
            s.data[s.top] = t; // 再把打出的牌放到桌上,即入栈
            book[t] = 1;       // 标记桌上现在已经有牌面为t的牌
        }
        else
        {
            // 小哼此轮可以赢牌
            q1.head++;         // 小哼已经打出一张牌,所以要把打出的牌出队
            q1.data[q1.tail] = t; // 紧接着把打出的牌放到手中牌的末尾
            q1.tail++;
            while (s.data[s.top] != t) // 把桌上可以赢得的牌依次放到手中牌的末尾
            {
                book[s.data[s.top]] = 0; // 取消标记
                q1.data[q1.tail] = s.data[s.top]; // 依次放入队尾
                q1.tail++;
                s.top--; // 栈中少了一张牌,所以栈顶要减1
            }
        }

        t = q2.data[q2.head]; // 小哈出一张牌
        // 判断小哈当前打出的牌是否能赢牌
        if (book[t] == 0) // 表明桌上没有牌面为t的牌
        {
            // 小哈此轮没有赢牌
            q2.head++; // 小哈已经打出一张牌,所以要把打出的牌出队
            s.top++;
            s.data[s.top] = t; // 再把打出的牌放到桌上,即入栈
            book[t] = 1;       // 标记桌上现在已经有牌面为t的牌
        }
        else
        {
            // 小哈此轮可以赢牌
            q2.head++;         // 小哈已经打出一张牌,所以要把打出的牌出队
            q2.data[q2.tail] = t; // 紧接着把打出的牌放到手中牌的末尾
            q2.tail++;
            while (s.data[s.top] != t) // 把桌上可以赢得的牌依次放到手中牌的末尾
            {
                book[s.data[s.top]] = 0; // 取消标记
                q2.data[q2.tail] = s.data[s.top]; // 依次放入队尾
                q2.tail++;
                s.top--;
            }
        }
    }

    if (q2.head == q2.tail)
    {
        printf("小哼win\n");
        printf("小哼当前手中的牌是");
        for (i = q1.head; i <= q1.tail - 1; i++)
            printf(" %d", q1.data[i]);
        if (s.top > 0) // 如果桌上有牌则依次输出桌上的牌
        {
            printf("\n桌上的牌是");
            for (i = 1; i <= s.top; i++)
                printf(" %d", s.data[i]);
        }
        else
            printf("\n桌上已经没有牌了");
    }
    else
    {
        printf("小哈win\n");
        printf("小哈当前手中的牌是");
        for (i = q2.head; i <= q2.tail - 1; i++)
            printf(" %d", q2.data[i]);
        if (s.top > 0) // 如果桌上有牌则依次输出桌上的牌
        {
            printf("\n桌上的牌是");
            for (i = 1; i <= s.top; i++)
                printf(" %d", s.data[i]);
        }
        else
            printf("\n桌上已经没有牌了");
    }

    getchar();
    getchar();
    return 0;
}

        如果你设计一些测试数据来验证的话,会发现我们刚才的代码其实还是有问题的。比如 游戏可能无法结束。就是小哼和小哈可以永远玩下去,谁都无法赢得对方所有的牌。

        游戏无法结束的问题可能是因为在某些情况下,小哼和小哈的牌在互相赢取过程中形成了一个循环,导致游戏无法达到终止条件。为了解决这个问题,可以引入一个计数器来限制游戏的轮数,当达到一定的轮数后,游戏自动结束,宣布平局。这样可以避免游戏陷入无限循环的情况。


链表

         首先我们来看一下,链表中的每一个结点应该如何存储。 每一个结点都由两个部分组成。左边的部分用来存放具体的数值,那么用一个整型变量 就可以;右边的部分需要存储下一个结点的地址,可以用指针来实现(也称为后继指针)。 这里我们定义一个结构体类型来存储这个结点,如下。

struct node 
{ 
    int data; 
    struct node *next; 
};

        上面代码中,我们定义了一个叫做 node 的结构体类型,这个结构体类型有两个成员。 第一个成员是整型 data,用来存储具体的数值;第二个成员是一个指针,用来存储下一个结 点的地址。因为下一个结点的类型也是 struct node,所以这个指针的类型也必须是 struct node * 类型的指针。

        如何建立链表呢?首先我们需要一个头指针 head 指向链表的最开始。当链表还没有建 立的时候头指针 head 为空(也可以理解为指向空结点)。

struct node *head; 
head = NULL;//头指针初始为空

        现在我们来创建第一个结点,并用临时指针 p 指向这个结点。

struct node *p; 
//动态申请一个空间,用来存放一个结点,并用临时指针p指向这个结点
p = (struct node*)malloc(sizeof(struct node)); 

        接下来分别设置新创建的这个结点的左半部分和右半部分。

scanf("%d",&a); 
p->data = a;//将数据存储到当前结点的data域中
p->next = NULL;//设置当前结点的后继指针指向空,也就是当前结点的下一个结点为空

        下面来设置头指针并设置新创建结点的*next 指向空。头指针的作用是方便以后从头遍 历整个链表。

if(head == NULL) {
    head=p;//如果这是第一个创建的结点,则将头指针指向这个结点
}
else {
    q->next = p;//如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点
}

        最后要将指针 q 也指向当前结点,因为待会儿临时指针 p 将会指向新创建的结点。

q = p;//指针q也指向当前结点

        完整代码如下:

#include <stdio.h>
#include <stdlib.h>

// 这里创建一个结构体用来表示链表的结点类型
struct node
{
    int data;
    struct node *next;
};

int main()
{
    struct node *head, *p, *q, *t;
    int i, n, a;
    scanf("%d", &n);
    head = NULL; // 头指针初始为空
    // 循环读入n个数
    for (i = 1; i <= n; i++)
    {
        scanf("%d", &a);
        // 动态申请一个空间,用来存放一个结点,并用临时指针p指向这个结点
        p = (struct node *)malloc(sizeof(struct node));
        p->data = a;        // 将数据存储到当前结点的data域中
        p->next = NULL;     // 设置当前结点的后继指针指向空,也就是当前结点的下一个结点为空
        if (head == NULL) {
            head = p;       // 如果这是第一个创建的结点,则将头指针指向这个结点
        }
        else {
            q->next = p;    // 如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点
        }
        q = p;              // 指针q也指向当前结点
    }
    // 输出链表中的所有数
    t = head;
    while (t != NULL)
    {
        printf("%d ", t->data);
        t = t->next;        // 继续下一个结点
    }
    getchar();
    getchar();
    return 0;
}

        接下来需要往链表中插入 6,操作如下。 首先用一个临时指针 t 从链表的头部开始遍历。 t=head;//从链表头部开始遍历 等到指针 t 的下一个结点的值比 6 大的时候,将 6 插入到中间。即 t->next->data 大于 6 时进行插入,代码如下。

scanf("%d", &a); // 读入待插入的数
while (t != NULL) // 当没有到达链表尾部的时候循环
{
    if (t->next->data > a) // 如果当前结点下一个结点的值大于待插入数,将数插入到中间
    {
        p = (struct node *)malloc(sizeof(struct node)); // 动态申请一个空间,用来存放新增结点
        p->data = a;
        p->next = t->next; // 新增结点的后继指针指向当前结点的后继指针所指向的结点
        t->next = p;       // 当前结点的后继指针指向新增结点
        break;             // 插入完毕退出循环
    }
    t = t->next; // 继续下一个结点
}

        完整代码如下。

#include <stdio.h>
#include <stdlib.h>

struct node
{
    int data;
    struct node *next;
};

int main()
{
    struct node *head, *p, *q, *t;
    int i, n, a;

    scanf("%d", &n);
    head = NULL; // 头指针初始为空

    for (i = 1; i <= n; i++) // 循环读入n个数
    {
        scanf("%d", &a);
        // 动态申请一个空间,用来存放一个结点,并用临时指针p指向这个结点
        p = (struct node *)malloc(sizeof(struct node));
        p->data = a; // 将数据存储到当前结点的data域中
        p->next = NULL; // 设置当前结点的后继指针指向空,也就是当前结点的下一个结点为空

        if (head == NULL)
        {
            head = p; // 如果这是第一个创建的结点,则将头指针指向这个结点
        }
        else
        {
            q->next = p; // 如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点
        }
        q = p; // 指针q也指向当前结点
    }

    scanf("%d", &a); // 读入待插入的数
    t = head; // 从链表头部开始遍历
    while (t != NULL) // 当没有到达链表尾部的时候循环
    {
        if (t->next->data > a) // 如果当前结点下一个结点的值大于待插入数,将数插入到中间
        {
            p = (struct node *)malloc(sizeof(struct node)); // 动态申请一个空间,用来存放新增结点
            p->data = a;
            p->next = t->next; // 新增结点的后继指针指向当前结点的后继指针所指向的结点
            t->next = p; // 当前结点的后继指针指向新增结点
            break; // 插入完毕退出循环
        }
        t = t->next; // 继续下一个结点
    }

    // 输出链表中的所有数
    t = head;
    while (t != NULL)
    {
        printf("%d ", t->data);
        t = t->next; // 继续下一个结点
    }
    getchar();
    getchar();
    return 0;
}


模拟链表

        链表中的每一个结点只有两个部分。我们可以用一个数组 data 来存储每序列中的每一个数。那每一个数右边的数是谁,这一点该怎么解决呢?上一节中是使用指针来解决的,这里我们只需再用一个数组right来存放序列中每一个数右边的数是谁就可以了,具体怎么做呢?

        两个数组data和right,第一个整型数组 data 是用来存放序列中具体数字的,另外一个整型数组 right 是用来存放当前序列中每一个元素右边的元素在数组 data 中位置的。例如 right[1] 的值为 2,就表示当前序列中 1 号元素右边的元素存放在 data[2]中;如果是 0,例如 right[9] 的值为 0,就表示当前序列中 9 号元素的右边没有元素。

        现在需要在 8 前面插入一个 6,只需将 6 直接存放在数组 data 的末尾即 data[10]=6。接下 来只需要将right[3]改为10,表示新序列中3号元素右边的元素存放在data[10]中。再将right[10] 改为 4,表示新序列中 10 号元素右边的元素存放在 data[4]中。这样我们通过 right 数组就可以 从头到尾遍历整个序列了(序列的每个元素的值存放在对应的数组 data 中),如下。

        完整的代码实现如下:


#include <stdio.h>

int main() {
    int data[101], right[101];
    int i, n, t, len;

    // 读入已有的数
    scanf("%d", &n);
    for (i = 1; i <= n; i++) {
        scanf("%d", &data[i]);
    }
    len = n;

    // 初始化数组right
    for (i = 1; i <= n; i++) {
        if (i != n) {
            right[i] = i + 1;
        } else {
            right[i] = 0;
        }
    }

    // 直接在数组data的末尾增加一个数
    len++;
    scanf("%d", &data[len]);

    // 从链表的头部开始遍历
    t = 1;
    while (t != 0) {
        if (data[right[t]] > data[len]) { // 如果当前结点下一个结点的值大于待插入数,将数插入到中间
            right[len] = right[t]; // 新插入数的下一个结点标号等于当前结点的下一个结点编号
            right[t] = len; // 当前结点的下一个结点编号就是新插入数的编号
            break; // 插入完成跳出循环
        }
        t = right[t];
    }

    // 输出链表中所有的数
    t = 1;
    while (t != 0) {
        printf("%d ", data[t]);
        t = right[t];
    }

    getchar();
    getchar();
    return 0;
}

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值