( 从零开始的数据结构生活 )二、栈


今天我们一起学习栈。
代码部分我依旧引用了印度老哥Harsha Suryanarayana的代码,并对学习过程中有问题的点进行描述。
当然,从这篇文章开始,我会说明我的原创点。
(1)原创点:
第二节:
解释了平均时间复杂度的计算过程。
对top变量做范围检查。
第三节:
比较了链表和链栈,解释了链栈的优点。
第四节:
在原视频中,Harsha是用C++,并使用了stack,在这里我用c语言进行描述。

(2) 本文分为5个部分:


1. 栈的介绍


定义:
是一种列表或者集合,并且具有如下的限制——插入或删除元素只能从一端,即栈顶进行。
操作:
(1)Push:向栈顶推入元素。
(2)PoP:从栈上移除最近插入的元素。
(3)Top:简单地返回栈顶的元素。
(4)IsEmpty:判断栈是否为空。
以上操作的时间复杂度都 为O(1)。


2.用数组实现栈


时间复杂度的分析:
(1)原因:
数据结构中有一个重点——时间复杂度,我个人不倾向使用数组,因为数组的规则导致了它不够灵活,相比其它数据结构,最基础的它更容易发生数据溢出,所以在使用数组构建栈之前,分析代码的时间复杂度是很有必要的;
(2)最好情况:
最好情况很容易理解,往栈顶放入数据即可,所以其时间复杂度为O(1);
(3)最坏情况:
数据溢出,即栈空间已经满了,但是系统还要给栈分配数据,那么需要重新建立一个更大的数组,通常建立的新数组的长度是原数组长度的2倍,我们需要把原数组中的元素转移到新的数组中,所以其时间复杂度为O(n);
(4)平均情况:
平均情况下,其时间复杂度为O(1),接下来我对此进行解释:插入的主要情况只有两种,原数组空间未满,进行操作和原数组空间已满,需要创建新的数组;
对两种情况进行细分,有n种情况:长度0~n-1和插满后的长度n,所以插入任意位置的概率为1/n,
那么我们对其进行加权平均法,得出其时间复杂度为O(1)。


定义和全局变量:


 	#define MAX_SIZE 101
	int A[MAX_SIZE];
	int top = -1;

代码解释:
第1行和第2行进行创建一个长度为101的数组;
第3行的 int top = -1 指创建一个空栈。


Push函数:

void Push (int x) 
{
	if (top == MAX_SIZE - 1)
	{
		printf("出现错误\n");
		return;
	}
	A[++top] = x;
}

构建一个函数,首先考虑会出现错误的情况,所以通过if进行判断,如果 top 在数组的倒数第二个节点之前,可以进行入栈,如果 top >= 100,那么我们为了避免栈的溢出,我们对其进行跳出;
在数组范围内,接下来进行元素的推入。


Pop函数:

void Pop( )
{
	if (top == -1)
	{
		printf("出现错误\n");
		return;
	}
	top--;
}

还是考虑函数会出现错误的情况,所以先通过if进行判断,如果栈为空栈,那么对其进行跳出;
满足上述条件,我们对数组进行自减。


Top函数:

void Top( )
{
	if(top<=100&&top>=0)
		return A[top];
	else 
	{
		printf("对变量进行范围检查\n");
		return;
	}
}

Top函数不用多说,返回栈顶元素,我对此做了些改动:我在原代码的基础上对 top 变量做了范围检查。
想必大家都没有问题。


3.用链表实现栈


我们把链表实现的栈称为链栈。
(1)使用链栈的原因:
数组实现栈的缺陷我已经在上一章讲解过了,接下来,我们通过链表实现栈。
我们先讲一讲需要链栈的原因,以下都是建立在相比于顺序栈的情况下:
链栈可以利用很多零碎的空间,节省空间且易变化;
也许链栈查询速度不如顺序栈更快,但是链栈添加和删除数据会更快一些。


(2)链栈插入节点的方式:
通过链的学习,我们会插入头节点和尾节点了,且知道头部的时间复杂度为O(1),尾部的时间复杂度为O(1);
而栈中 Push 和 Pop 函数的时间复杂度都为O(1),所以我们不会用尾部插入的方式,而是选择头部插入的方式。


结构体

struct Node
{
	int data;
	struct Node* next;
};
struct Node* top = NULL;

Push函数:

void Push(int x)
{
	struct Node* temp = (strcut Node*)malloc(sizeof(struct Node));
	temp->data = x;
	temp->link = data;
	top = temp;
}

代码解释:
首先在堆上分配空间,关于堆空间的分配,用以下式子表明:
指针自身 = (指针类型*)malloc(sizeof(指针类型)* 数据数量);
其次进行赋值,并且把 top 的地址传入 temp->link 因为 top 的地址是NULL,那么此时 temp->link 的地址因为是NULL;
最后我们把 temp 的地址传入 top 中。
其实这段代码和链的头插入一模一样的,如果对链表很熟悉,那么这段学习起来代码没有压力。


Pop函数:

void Pop()
{
	struct Node* temp;
	if(top == NULL)
		return;
	temp = top;
	top =top->link;
	free(temp);
}

代码解释 :
先建立一个临时的空指针;
判断 top 是否为空栈,如果是,退出,如果不是那么删除该元素;
将栈上第一个元素给到临时性的 temp 指针上,然后再使 top 指针指向栈上的第二个元素;
最后释放 temp 这个临时性的指针。


4.反转字符串和链表


(1)反转字符串


Reverse函数:

void Reverse(char *c,int n)
{
	int i = 0;
	for (i = 0; i < n; i++)
	{
		Push(c[i]);
	}
	for (i = 0; i < n; i++)
	{
		c[i] = top->data;
		Pop();
	}
}

代码解释:
这个是有前提的,需要把前面结构体 中的 data 前面的类型 int 改成 char;
首先引入变量 i 进行循环;
其次往栈里面存贮元素;
最后将栈中的元素放回字符串中。
值得注意的是,需要在赋值后,Pop 掉栈顶,以便下一次循环的赋值。


完整代码:


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

struct Node
{
	char data;
	struct Node* link;
};

struct Node* top = NULL;

void Push(char x)
{
	struct Node* temp = (struct Node*)malloc(sizeof(struct Node));
	temp->data = x;
	temp->link = top;
	top = temp;
}

void Pop()
{
	struct Node* temp;
	if (top == NULL)
	{
		return;
	}
	temp = top;
	top = temp->link;
	free(temp);
}

void Reverse(char *c,int n)
{
	int i = 0;
	for (i = 0; i < n; i++)
	{
		Push(c[i]);
	}
	for (i = 0; i < n; i++)
	{
		c[i] = top->data;
		Pop();
	}
}

int main()
{
	char c[51];
	printf_s("请输入字符串");
	gets_s(c);
	Reverse(c, strlen(c));
	printf_s("输出 = %s",c);
}

(2)反转链表:


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

struct Node
{
	int data;
	struct Node* link;
};

struct lian
{
	int data;
	struct lian* link;
};

struct Node* top = NULL;
struct lian* head = NULL;
struct lian* prev = NULL;

void Push(int n)
{
	struct Node* temp = (struct Node*)malloc(sizeof(struct Node));
	temp->data = n;
	temp->link = top;
	top = temp;
}

void Pop()
{
	struct Node* temp;
	if (top == NULL)
	{
		return;
	}
	temp = top;
	top = temp->link;
	free(temp);
}

void Insert(int x)
{
	struct lian* temp = (struct lian*)malloc(sizeof(struct lian));
	if (head == NULL)
	{
		head = temp;
		prev = temp;
		temp->data = x;
		temp->link = NULL;
		return;
	}
	prev->link = temp;
	temp->data = x;
	temp->link = NULL;
	prev = temp;
}

void Print()
{
	struct lian* temp = head;
	while (temp != NULL)
	{
		printf("%d ", temp->data);
		temp = temp->link;
	}
	printf("\n");
}

void Reverse()
{
	lian* temp = head;
	while(temp != NULL)
	{
		Push(temp->data);
		temp = temp->link;
	}
	free(temp);
	head = NULL;
	while(top != NULL)
	{
		Insert(top->data);
		Pop();
	}
}

int main()
{
	Insert(2);
	Insert(4);
	Insert(6);
	Insert(8);
	printf_s("反转前");
	Print();
	Reverse();
	printf_s("反转后");
	Print();
}

代码解释:
以上我直接贴出完整的代码,而本篇代码最重要的就是尾节点插入函数,注意,这里是不能使用头节点插入法,因为这样是不反转链栈的。
这里我讲解一下 Insert 函数:
首先有两个全局变量,head perv ;
如果 head 是空指针,那么就让 两个 指针都指向 temp ;
我们还是自定义:把前一个 temp 称 temp1 ,现在的 temp 称 temp2,接下来的 temp 称 temp3;
接下来,如果 head 已经有指向了,那么对 temp->data 进行赋值, 再让temp->link 指向NULL;而上面的 prev->link 是用来连接 temp1 和 temp2 的;(这一块可能有点难理解,读者多画链表图)
最后让 prev 指向现在的 temp,也就是 temp2 ,为 temp3 的连接做准。


5.前缀、中缀、后缀


我们先来看看什么叫中缀,前缀和后缀,但是这一篇章我不会进行代码的制作,因为C语言并不如C++好实现,所以我进行忽略,等到读者学习C++后,可以学习。
(1)定义:
中缀:人们理解意义上的运算法则,例如——(4+7)* 8;
前缀:* + 4 7 8,从右向左,进行入栈,遇见运算符,将栈顶的两个数字出栈运算,计算后将结果入栈;
后缀:4 7 + 8 * ,从左向右,进行入栈,遇见运算符,将栈顶的两个数字出栈运算,计算后将结果入栈;
对于人们而言,很容易理解中缀的运算方式,而对于计算机而言,它们更容易理解前缀和后缀的方式。


(2)
这里我并未给出代码,是因为C++对此进行操作更方便,而C却要大量的,一系列的自我定义,后续我可能会再出一起如何用C++编程。


*最后以上内容就是这么多,希望读者多加练习,会很容易了解数据结构的。
Harsha Suryanarayana的熟肉
同时也感谢up主fengmuzi2003的翻译。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值