何为数据结构?
数据结构是一种在程序中系统化管理数据集合的形式
数据结构很少单纯地表示数据集合,它通常由以下3个概念组合而成:
1.数据集合
通过对象数据的本体(例如数组和结构体等基本数据结构)保存数据集合
2.规则
保证数据集合按照一定规矩进行正确的操作、管理和保存的规则,比如按照何种顺序取出数据等条款
3.操作
“插入元素”,“取出元素”等对数据集合的操作。“查询数据的元素数”和“检查数据集合是否为空”等查询也包含在内
栈
定义:
栈是一种数据结构,其按照最后进入栈的数据最先出栈的规则管理数据(Last In First Out,LIFO)
其中栈中最后一个元素的下一个位置称为栈顶,相应地也存在栈底,当栈顶到达预先开好的栈的空间的时候,称为栈满;当栈内没有任何元素的时候,称为栈空
一开始我们可能不理解,这样的奇怪的数据结构特点有什么作用呢?没关系,我们就先记住,后面在使用栈的时候自然就明白了
对栈的操作:
push(x):在栈顶添加元素x
pop(x):从栈顶部取出元素
isEmpty():检查栈是否为空
isFull():检查栈是否已满
另:
引用栈顶元素
检查栈中是否含有指定数据
规则:
数据中最后加入的元素最先被取出,即pop取出的元素时最后一次被push进入栈的元素
栈的实现:
1.数组实现
通过数组,我们很容易实现栈
只需要开一个长度为n的空数组,栈底即为下标0,再开一个变量top作为栈顶,top初始化为-1,此时为栈空;当想要push进一个元素进栈时,top++并插入;当top的值为n-1的时候,栈满
同时,为了方便开多个栈,我们将栈的数组和top变量整合为一个结构体
实现代码如下:
#define MAX 100
typedef struct stack{
int nums[MAX];
int top;
}Mystack;
//初始化
void InitiatStack(Mystack* stack) {
stack->top = -1;
}
//进栈
void push(Mystack* stack, int x) {
stack->top++;
stack->nums[stack->top] = x;
}
//出栈
int pop(Mystack* stack) {
stack->top--;
return stack->nums[stack->top + 1];
}
//检验栈空
bool isEmpty(Mystack* stack) {
return stack->top==-1;
}
//检验栈满
bool isFull(Mystack* stack) {
return stack->top == MAX - 1;
}
int main() {//检验函数是否成立
Mystack* a = (Mystack*)malloc(sizeof(Mystack));
InitiatStack(a);
for (int i = 0; i < MAX; i++) {
push(a, i);
}
printf("是否为空:%d,是否为满:%d\n", isEmpty(a),isFull(a));
for (int i = 0; i < MAX; i++) {
printf("%d ", pop(a));
}
printf("\n是否为空:%d,是否为满:%d\n", isEmpty(a),isFull(a));
return 0;
}
2.链表实现
观察栈的运作方式,我们发现,当元素进栈时,我们只需要关注栈顶的位置;当元素出栈时,我们只需要关注栈顶及栈顶前一个元素的位置,因此,我们可以设置一个top指针,指向栈顶节点;还有一个bottom指针,指向栈底,同时使链表中各个节点的next指针指向前一个元素即可
实现代码如下:
typedef struct stack2 {
int val;
struct stack2* next;
}StackNode/*栈节点*/, * LinkStackPtr;//节点指针
typedef struct LinkStack{//栈顶结构体
LinkStackPtr top;//指向栈顶的指针
int count;
}LinkStack;
StackNode* Initiat2() {
StackNode* stack = (StackNode*)malloc(sizeof(StackNode));
stack->next = NULL;//创建栈并初始化
return stack;
}
void push2(LinkStack* p,int n) {//进栈
LinkStackPtr new = (LinkStackPtr)malloc(sizeof(StackNode));
new->val = n;
new->next = p->top;
p->top = new;
p->count++;
}
int pop2(LinkStack* p) {//出栈(栈非空的前提)
LinkStackPtr temp = p->top;
p->top = p->top->next;
int res = temp->val;
free(temp);
p->count--;
return res;
}
bool isEmpty2(LinkStack* p) {//检验栈空
return !p->top->next;
}
int main() {//检验函数是否成立
StackNode* stack = Initiat2();//创建栈
LinkStack* p = (LinkStack*)malloc(sizeof(LinkStack));
p->top = stack;
p->count = 0;//初始化栈顶指针
printf("是否为空:%d\n", isEmpty2(p));
for (int i = 0; i < 10; i++) {
push2(p, i);
}
printf("是否为空:%d\n", isEmpty2(p));
for (int i = 0; i < 10; i++) {
printf("%d\n", pop2(p));
}
printf("是否为空:%d\n", isEmpty2(p));
}
用链表实现栈,我们可以无限制地往栈内push元素(只要内存足够容纳),因此使用起来十分方便
栈的应用示例:
1.括号问题:
引入力扣的一道题目:
其中字符串仅由{}[]()这六个元素组成
想要处理这道题目,我们就必须找到组成有效括号的特征,也就是每一个左括号都必须要有相应地右括号与之闭合,关键之处就在于他们是怎样闭合的,也就是说闭合的时候他们括住的元素也要是闭合的,如果我们这样关注多个对象来考虑,那就没完没了了
因此,我们不妨举几个字符串,看下能不能发现它们的共同特征:
((())) , ({}[]) , ([{})(])
将他们的括号高度错开,我们看下会发生什么:
( ) ( ) ( )
( ) { }[ ] [ ]
() , , { }) (
我们从左到右地观察左括号,发现最影响我们判断有效括号的就是几个相同类型括号的嵌套,如何区分它们是否有效呢?
解决问题的关键就在于:从左往右遍历所有的左括号,我们会发现,最后遍历到的左括号一定最先和右括号闭合
这就类似于我们栈的运作,最后进入的元素,我们最先检验,当它成立时,就将它pop出
因此,我们只需遍历字符串,当遇到左括号的时候,将其push入栈;当遇到右括号的时候,检查栈顶是否有与之对应的左括号,若没有,则括号无效,返回false;当字符串被全部遍历完毕的时候,也就是栈中没有剩下任何元素的时候,返回true;
实现代码如下:
char pair(char a) {//为了更好地判断括号匹配,我们建立一个pair函数
if (a == '}') return '{';
if (a == ']') return '[';
if (a == ')') return '(';
return 0;
}
bool isValid(char* s) {
int n = strlen(s);
if (n % 2 == 1) {//当字符串元素个数为奇数的时候,括号一定无效
return false;
}
int* stack=(int*)malloc(sizeof(int)*n);
int top = -1;//建立栈数组
for (int i = 0; i < n; i++) {//遍历字符串
char ch = pair(s[i]);
if (ch) {//当s[i]为右括号时
if (top == -1 || stack[top] != ch) {//当栈为空,或者括号不匹配时
return false;
}
top--;//括号匹配时,pop出栈顶的右括号
} else {//当s[i]为左括号时
stack[++top] = s[i];
}
}
free(stack);
return top == -1;
}
2.括号问题变式:
我们引入这样一道题目:
一开始拿到这道题目,我们可能会对水坑面积的求法无从下手,但其实这是一道括号问题的变式
水坑由\和/组成,遍历同一高度的\和/,后遇到的\一定会先与/形成水坑,而他们之间包括的面积就等于他们的下标之差,(_占了一个下标的长度,所以刚好不用管它,碰到跳过即可),也就是说,我们可以利用括号问题的方法,求出每一个高度的\和\围成的面积并push入栈,相加起来,就求出了总面积
那么我们怎么求出有多少个水坑,以及各个水坑的面积呢?
我们观察几个小面积能够合成一个水坑的大面积需要什么条件
对于一个大水坑,我们先找到其高度高的面积,那么他的长度肯定也是最长的,再往下看高度较低的面积,观察一下他们的长度关系
我们记面积s1=j1-i1,其中j1为/的下标,i1为\的下标;s2为j2-i2;s1的高度大于s2,且它们位于同一个水坑的时候,我们肯定有:i2>i1且j2<j1
我们还可以再简化,其实只要满足j2<j1即可
因此,我们需要再建立一个栈,同时记录压入的面积s和下标i(知道s和i就可以求j)
在我们建立的栈中,各部分的面积其实是按水坑顺序拍好的,一个水坑内的面积又是按由低到高排好的,因此我们可以很容易地合并每个水坑的面积并保存
实现代码如下:
typedef struct stack {
int left;
int area;
struct stack* next;
}stack2;
void StackPush(stack2** p,int left, int area) {
stack2* new = (stack2*)malloc(sizeof(stack2));
new->left = left;
new->area = area;
new->next = *p;
*p = new;
}
int* Areas(char* slope, int* total,int*returnSize) {
*total = 0;
*returnSize = 0;
int* stack1 = (int*)malloc(sizeof(int) * strlen(slope));
stack2* tail = (stack2*)malloc(sizeof(stack2));
tail->next = NULL;
stack2* head = tail;
int top1 = 0;
for (int i = 0; i < strlen(slope); i++) {
if (slope[i] == '\\') {
stack1[top1++] = i;
}
else if (slope[i] == '/' && top1 != 0) {
top1--;
StackPush(&head, stack1[top1], i - stack1[top1]);
*total += head->area;
}
}
int* ans = (int*)malloc(sizeof(int) * (strlen(slope) / 2));
int i = 0;
while (head != tail) {
int length = head->area + head->left;
int LEFT = head->left;
while (head->next->left < length&&head->next->left>LEFT) {
stack2* p = head;
head = head->next;
head->area += p->area;
free(p);
}
ans[i++] = head->area;
(*returnSize)++;
head = head->next;
}
return ans;
}
int main() {//检验
char str[38] = "\\\\///\\_/\\/\\\\\\\\/_/\\\\///__\\\\\\_\\\\/_\\/_/\\";
int* total = (int*)malloc(sizeof(int));
int* returnSize = (int*)malloc(sizeof(int));
int* res = Areas(str, total, returnSize);
printf("count total:%d\n", *returnSize);
for (int i = 0; i < *returnSize; i++) {
printf("the %d area:%d\n", i+1,res[i]);
}
printf("area total:%d\n", *total);
return 0;
}