数据结构与算法实战:栈

数据结构与算法实战:栈

数组与指针

数组名与首元素地址

运行以下代码,我们发现数组名和数组的首元素地址是相同的,那我们可以认为数组名就是首元素地址吗?其实不然,我们通过以下代码来验证一下:

#include<stdio.h>
int main()
{
	int arr[5] = {1,2,3,4,5 };
	printf("%d\n", sizeof(arr));
	printf("%p\n", arr);
	printf("%p\n", &arr[0]);
	return 0;
}

在这里插入图片描述

如果首字母是首元素地址的话,那么使用sizeof()运算符所求得的值应该为一个指针的大小,即求得的值为4,可结果却为20,为整个arr数组的大小,所以可以证明,数组名不是首元素的地址,但是为什么以指针变量的形式打印数组名和数组名首地址时,两者的结果相同呢?C99标准是这样定义的:

除了在使用sizeof&运算符或者使用字符串字面量初始化数组之外,一个含有数组名的表达式会转化为含有指向首元素的表达式,并且转化后不是一个左值。如果数组的存储类型是寄存器的话,行为是未定义的。

所以我们可以这样去理解数组名:数组名不是首元素地址,但对数组名求值得到的是首元素地址!

利用指针访问数组

我们知道,数组是在内存中开辟的一段连续的内存空间,数组中的各元素在内存中是连续分布的,要想访问数组中某一元素,那么就必须知道其地址。我们声明数组之后,使用操作符[]+数组下标的方式去访问数组,那[]的实质是什么呢? 我们先给出结论:[]不过是个运算符而已

p[i]<==>*(p+i) i[p]<==>*(i+p)

在理解访问利用指针访问之前,我们首先得先理解另一个问题:定义一个数组arr,按前面所说,对arr求值时就是数组第一个元素的首地址,那么arr+1是什么呢?是一个地址加一个数字1吗?还是有其他的理解?

int main()
{
	int arr[5] = {1,2,3,4,5 };
	printf("arr=%p\n", arr);
	printf("arr+1=%p\n",arr+1);
	printf("arr+2=%p\n",arr + 2);
	printf("*arr=%d\n", *(arr));
	printf("*(arr+1)=%d\n", *(arr + 1));
	printf("*(arr+2)=%d\n", *(arr + 2));
	return 0;
}

在这里插入图片描述

根据测试可知,a+1并非是指针a加数字1,a+2并非是指针a加数字2,实际上他们的地址两两之间相差是4,而指针变量正好为4个字节,即两者正好相差一个指针,所以当我们对指针变量进行+i的时候,实际上是将指向该变量的指针向后移动了i个位置,所以,a[0]=*(a+0)=1,a[1]=*(a+1)=2……由此可以知道,a+i实际上就是a的第i个元素的地址。

我们在来一段代码证明一下另一个结论i[p]<==>*(i+p)

#include<stdio.h>
int main()
{
	int arr[5] = {1,2,3,4,5 };
	printf("%d\n", *(arr+1));
	printf("%d\n", arr[1]);
	printf("%d\n", 1[arr]);
	return 0;
}

在这里插入图片描述

不过,在访问数组时,我们一般不写为可读性较差的1[arr],而写为arr[1]。

动态申请数组

malloc

malloc()函数:该函数接受一个参数:所需的内存字节数,malloc()函数会找到合适的空闲内存块,这样的内存是匿名的。也就是说,malloc()分配内存,但是不会为其赋名。然而,它确实返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。因为char表示1字节,malloc()的返回类型通常被定义为指向char的指针。然而,从ANSI C标准开始,C使用一个新的类型:指向void的指针。该类型相当于一个**“通用指针”**。malloc()函数可用于返回指向数组的指针、指向结构的指针等,所以通常该函数的返回值会被强制转换为匹配的类型。在ANSI C中,应该坚持使用强制类型转换,提高代码的可读性。如果 malloc()分配内存失败,将返回空指针。

动态申请数组

利用我们前面铺垫的数组与指针的知识,我们可以申请一块连续的内存空间,并用一个指针变量(假设为p)来接收malloc()函数返回来的地址,这样我们就可以像使用数组一样使用它§,把p当做数组名来使用。也就是说p[下标]的方式去使用数组。

int main()
{
	int* p;
	int n = 5;//定义数组长度
	p = (int*)malloc(n * sizeof(int));//内存大小为:数组长度×数组的元素类型所占字节数
	p[1] = 233;//使用数组,给数组元素赋值
	printf("p[i]=%d", p[1]);
	return 0;
}

在这里插入图片描述

free

**free()函数:**该函数的参数是之前malloc()返回的地址,该函数释放之前malloc()分配的内存。如果参数指向的空间不是动态开辟的,那free函数的行为是未定义的,如果参数是NULL指针,则函数什么事都不做。一个好习惯是:我们动态分配的内存在使用完毕后及时去释放。

栈的基本理论

栈的定义

栈(stack)又名堆栈,它是一种运算受限的线性表。仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底,遵循先入后出,后入先出的结构。

在这里插入图片描述

栈的操作

  • 初始化(init):根据实际要求对栈进行初始化
  • 入栈(push):把新元素放到栈顶元素的上面,使之成为新的栈顶元素
  • 出栈(pop):把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素
  • 取栈顶元素(top):获取栈顶元素,不对栈进行改变

栈的操作实现

栈有两种实现类型,分别是采用链式存储结构的链式栈和采用顺序存储结构的顺序栈,链式栈我们可以使用带头指针的单链表轻松实现,以头指针的位置为栈顶,可以很方便的实现栈的所有操作,并且上一篇博客我们已经重点对单链表进行了解析,故在此不在赘述,本次我们选择用数组实现顺序栈。

定义栈

struct Stack { 
	int* data;
	int capacity;
	int top;
};

我们用结构体来定义栈,并且先不指定栈的大小,如果在定义时便对栈的大小进行了定义,那么使用栈时可能会和我们的实际要求不一样,所以我们希望在栈初始化的时候,根据需要动态分配栈的大小。

首先定义指针变量int* data,用来承接之后初始化栈时动态分配的数组的地址,上午以及花了大篇幅对此进行解释,这里不在赘述,capacity用来记录目前栈的容量,top用来指向栈顶元素的位置或者栈顶元素的上一个位置,初始化方式不同,指向的位置不同。

栈的初始化

先定义一个栈struct Stack st,给出调用方法init(&st,5);,传参时我们选择传递st的地址,因为我们希望初始化时去改变栈的内容(动态分配数组、栈的元素个数、栈顶元素的指向位置)

操作实现

void init(struct Stack *ps,int capacity )
{
	ps->capacity = capacity;
	ps->data = (int*)malloc(capacity*sizeof(int));
	ps->top = 0;
}

初始化为top为-1表示top指向最高元素 初始化top为0表示top指向最高元素之上的空格,两者没有什么区别,只是在判断栈空和栈满时的条件不同。

判断栈空

调用方式:isEmpty(ps),因为判断栈空的操作经常是被其他操作所调用的,而其他操作往往接收的参数是定义栈的指针,所以我们在实现判断栈空的操作时,也选择接收指针的方式实现,但我们不希望我们进行判空操作时对栈进行修改,所以我们用const进行修饰,并且该操作要对栈空还是非空进行判断,所以是有返回值的,所以我们将该函数定义为int型的,栈空返回1,栈非空返回0。

int isEmpty(const struct Stack* ps)
{
    return ps->top==0;
}

//我们在初始化栈时,top置位0,所以当top=0时,栈是空的,对于条件表达式,表达式为真时,表达式的值为1,表达式为假时,表达式的值为0,与我们函数设计思路不谋而合,所以我们直接这样实现return ps->top==0

判断栈满

调用方式:isFull(ps),判断栈满的函数设计思路与判空操作的原因基本相同,只是判断条件不同,因为我们初始化栈时,top是指向栈顶元素的上一个位置的,所以当top的值与栈的容量相等时,栈即为满的。例如:栈的容量为1时,top初始化为0,插入一个元素后,top为1,此时栈满,top的值正好与capacity相同,栈满。

操作实现

int isFull(const struct Stack* ps)
{
    return ps->top==ps->capacity;
}

入栈

函数的调用方法:push(&st, 11);,传入我们要操作的栈的指针,要入栈的元素;我们的操作是有可能失败或者有可能成功的,所以我们将入栈函数的返回值设计为int型,入栈成功返回1,入栈失败返回0

操作实现

int push(struct Stack* ps,int x)
{
    if(isFull(ps))
    {
        return 0;//栈满,入栈失败,返回0
    }
    else
    {
        ps->data[ps->top]=x;
        ps->top++;
        return 1;
    }
}

入栈时首先要判断栈是否满了,若栈满,则操作不合法,返回0结束,若栈不满,ps->data[ps->top]=x,我们在定义时将top指向了栈顶元素的上一个位置,所以我们在入栈时可以直接将插入的位置下标选为top,插入操作完成后top++,操作成功,返回1,结束操作。

出栈

函数的调用方法:pop(&st,&x),首先是传入我们要操作的栈的地址,并且传入我们操作之前定义好的变量的地址,假设为x,用于记录我们入栈的元素是什么,我们的操作是有可能失败或者有可能成功的,所以我们将出栈函数的返回值设计为int型,出栈成功返回1,出栈失败返回0

操作实现

int pop(struct Stack* ps,int* px)
{
    if(isEmpty(ps))
    {
        return 0;//栈空,出栈操作不合法,返回0
    }
    else
    {
        ps->top--;//向下移动,指向栈顶元素
        *px = ps->data[ps->top];//将栈顶元素返回给传入的x
        return 1;
        
    }
}

在我们定义栈的时候,设计时便定义top指向栈顶元素的上一个位置,所以当我们top--的时候便标志着栈顶元素改变为原来栈顶元素相邻的那一个元素了,实际上已经完成了出栈操作,同时标志着原来栈顶元素所在的这块空间在逻辑上是没有元素的,随时可以入栈重新在该位置插入元素。

取栈顶元素

函数的调用方法:top(&st, &x),首先是传入我们要操作栈的地址,其次是传入取栈顶元素之前我们定义的用于记录栈顶元素值的变量的地址,例如x,我们的操作是有可能失败有可能成功的,所以我们将取栈顶元素的函数的返回值设计为int型,操作成功返回1,操作失败返回0

int top(const struct Stack* ps, int* px)
{
	if (isEmpty(ps))
	{
		return 0;//栈空,取栈顶元素操作不合法,返回0
	}
	else
	{
		*px = ps->data[ps->top-1];//栈顶元素的位置为top位置的下一个位置
		return 1;//表示取栈顶元素操作成功
	}
}

*px = ps->data[ps->top-1],因为我们的top是栈顶元素的上一个位置,所以我们在去栈顶元素时一个去top-1位置的元素。

销毁栈

虽然程序结束以后,所占的空间会全部释放,但是有时候栈需要反复使用,反复初始化,程序运行中间应该就要做一下destory(),避免程序长期运行,反复调用,可能发生的内存泄漏,动态分配的内存在使用完成后及时去释放是个好习惯

void destroy(struct Stack* ps)
{
	free(ps->data);
}

效果测试

完整测试代码

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

struct Stack { 
	int* data;
	int capacity;
	int top;
};

init(struct Stack *ps,int capacity )
{
	ps->capacity = capacity;
	ps->data = (int*)malloc(capacity*sizeof(int));
	ps->top = 0;
}

int isEmpty(const struct Stack* ps)
{
    return ps->top==0;
}

int isFull(const struct Stack* ps)
{
    return ps->top==ps->capacity;
}

int push(struct Stack* ps,int x)
{
    if(isFull(ps))
    {
        return 0;//栈满,入栈失败,返回0
    }
    else
    {
        ps->data[ps->top]=x;
        ps->top++;
        return 1;
    }
}

int pop(struct Stack* ps,int* px)
{
    if(isEmpty(ps))
    {
        return 0;//栈空,出栈操作不合法,返回0
    }
    else
    {
        ps->top--;//向下移动,指向栈顶元素
        *px = ps->data[ps->top];//将栈顶元素返回给传入的x
        return 1;
        
    }
}


int top(const struct Stack* ps, int* px)
{
	if (isEmpty(ps))
	{
		return 0;//栈空,取栈顶元素操作不合法,返回0
	}
	else
	{
		*px = ps->data[ps->top-1];//栈顶元素的位置为top位置的下一个位置
		return 1;//表示取栈顶元素操作成功
	}
}


void destroy(struct Stack* ps)
{
	free(ps->data);
}


int main()
{
	struct Stack st;
	init(&st,5);
	push(&st, 11);
	push(&st, 22);
	push(&st, 33);
	push(&st, 44);
	push(&st, 55);
	int x;
	pop(&st,&x);
	printf("弹出栈顶元素%d\n", x);
	top(&st, &x);
	printf("现在的栈顶元素为%d\n", x);
	pop(&st, &x);
	printf("弹出栈顶元素%d\n", x);
	top(&st, &x);
	printf("现在的栈顶元素为%d\n", x);
	push(&st, 2333);
	printf("入栈\n");
	top(&st, &x);
	printf("入栈后,栈顶元素为%d\n",x);
	destroy(&st);
	return 0;
}

在这里插入图片描述

本文花了大量篇幅,数组开始讲起,穿插了动态分配数组的内容,为我们栈的实现做铺垫,希望读者可以领略到笔者的用心良苦,认真理解,同时由于笔者水平有限,文章可能出现一些错误,希望大家可以多多批评指正!在这里插入图片描述

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值