小肥柴慢慢手写数据结构(C篇)(3-1 Stack栈简介)
目录
3-1 栈的概念
一句话概括:压弹夹。看过动作片的朋友很容易就理解了,在这个场景中:
(1)压入子弹(Push)
(2)开枪消耗子弹/退子弹(Pop)
(3)子弹就是元素(Element)
(4)弹夹就是栈(Stack)
上图:
栈顶元素称为Top,栈底元素称为Button,其实也有很多别的称谓,意思大致都是一样的;新元素只能添加在现有栈顶的上方,取走的元素也只能是当前栈顶元素(那么下一个元素会自动变成新的栈顶)。
正规一点的概括栈,描述如下(参考 liuyubobo的ppt):
(1)栈是数组/链表的子集数据结构,也是线性的;
(2)常规的讲,栈只能从一端添加元素,也只能从相同的这端取出元素。
3-2 栈的动态数组实现
使用之前实现的动态数组,要点如下:
(1)仅在数组尾部进行push和pop操作;
(2)动态数组的标记topIndex标记top元素;
(3)pop操作并没有将元素真正剔除出数组,只是使用topIndex屏蔽了后续元素;
(4)push操作时,若现有数组满员,扩容;
(5)pop操作时现有数组使用元素低于容量上限的1/4,且当前容量高于默认容量,缩减数组容量。
- ArrayStack.h
#ifndef _ARRAY_STACK_H
#define _ARRAY_STACK_H
#define EMPTY_TOS (-1)
#define DEFAULT_CAPACITY (20)
typedef int ElementType;
struct StackRecord {
int capacity;
int topIndex;
ElementType *array;
};
typedef struct StackRecord *Stack;
int isEmpty(Stack S);
int isFull(Stack S);
Stack createStack();
void disposeStack(Stack S); //销毁栈
void makeEmpty(Stack S); //清空当前栈
void push(ElementType X, Stack S);
ElementType top(Stack S); //单纯获取头部元素
void pop(Stack S);
ElementType topAndPop(Stack S); //弹出元素并返回弹出元素内容
void printStack(Stack S);
#endif
- ArrayStack.c,代码简单配合要点说明很容易看明白
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "ArrayStack.h"
Stack createStack(){
Stack S = (Stack )malloc(sizeof(struct StackRecord));
if(!S)
return NULL;
S->array = malloc(sizeof(ElementType) * DEFAULT_CAPACITY);
if(!S->array){
free(S);
return NULL;
}
memset(S->array, 0, DEFAULT_CAPACITY);
S->capacity = DEFAULT_CAPACITY;
S->topIndex = EMPTY_TOS;
return S;
}
int isEmpty(Stack S){
return S->topIndex == EMPTY_TOS;
}
int isFull(Stack S){
return (S->topIndex + 1) == S->capacity;
}
void makeEmpty(Stack S){
S->topIndex = EMPTY_TOS;
}
void disposeStack(Stack S){
if(!S){
free(S->array);
free(S);
}
}
void reSize(Stack S, int size){
ElementType *arr = malloc(sizeof(ElementType) * size);
memset(arr, 0, size);
int i;
for(i = 0; i <= S->topIndex; i++)
arr[i] = S->array[i];
ElementType *oldArr = S->array;
S->array = arr;
S->capacity = size;
free(oldArr);
}
void push(ElementType X, Stack S){
if(isFull(S)) //满员,扩容
reSize(S, S->capacity*2);
//这里我比较喜欢,灵活应用前++和后++
S->array[++S->topIndex] = X;
}
void pop(Stack S){
if(!isEmpty(S)){
S->topIndex--;
int len = S->topIndex + 1;
if(len == S->capacity/4 && S->capacity > DEFAULT_CAPACITY)
reSize(S, S->capacity/2); //参考liuyubobo大佬的防抖缩编
}
}
ElementType top(Stack S){
return isEmpty(S) ? INT_MIN : S->array[S->topIndex];
}
ElementType topAndPop(Stack S){
return isEmpty(S) ? INT_MIN : S->array[S->topIndex--];
}
void printStack(Stack S){
if(isEmpty(S))
printf("\nEmpty!");
else{
printf("\n%d(top)", top(S));
int i = S->topIndex - 1;
while(i >= 0)
printf("\n%d", S->array[i--]);
}
}
- 测试
#include <stdio.h>
#include <stdlib.h>
#include "ArrayStack.h"
int main(int argc, char *argv[]) {
Stack stack = createStack();
int i;
printf("add 0~8:");
for(i=0; i<10; i+=2)
push(i, stack);
printStack(stack);
printf("\n\nadd 1~9:");
for(i=1; i<10; i+=2)
push(i, stack);
printStack(stack);
printf("\n\npop: ");
pop(stack);
printStack(stack);
printf("\n\npop 3times ");
pop(stack);
pop(stack);
pop(stack);
printStack(stack);
printf("\n\ntop=>%d", top(stack));
printStack(stack);
printf("\n\ntopAndPop=>%d", topAndPop(stack));
printStack(stack);
printf("\n\n cap before add overflow is %d", stack->capacity);
for(i=10; i<30; i++)
push(i, stack);
printf("\n\n cap after add overflow is %d", stack->capacity);
printStack(stack);
return 0;
}
3-3 栈的链表实现
要点如下:
(1)使用头插法和删除头结点方法对应实现push和pop操作;
(2)为了方便操作,使用虚拟头结点;
(3)为了减少遍历可能,维护一个节点计数器,方便查询节点个数和判断空栈;
(4)销毁栈要注意释放顺序。
注:实际上也可以使用双链表,或者添加尾部标记来实现,还是那句话,根据实际需要来选择。
- ListStack.h
#ifndef _LIST_STACK_H
#define _LIST_STACK_H
typedef int ElementType;
typedef struct Node{
ElementType Element;
struct Node *Next;
} Node;
typedef struct StackRecord{
Node *header;
int len;
} *Stack;
int isEmpty(Stack S);
int getSize(Stack S);
Stack createStack();
void disposeStack(Stack S);
void makeEmpty(Stack S);
void push(ElementType X, Stack S);
ElementType top(Stack S);
void pop(Stack S);
ElementType topAndPop(Stack S);
void printStack(Stack S);
#endif
- ListStack.c
#include <stdlib.h>
#include <stdio.h>
#include "ListStack.h"
Stack createStack(){
Stack stack = (Stack)malloc(sizeof(struct StackRecord));
if(!stack)
return NULL;
stack->header = (Node *)malloc(sizeof(struct Node));
if(!stack->header){
free(stack);
return NULL;
}
stack->header->Next = NULL;
stack->len = 0;
return stack;
}
int isEmpty(Stack S){
return S->len == 0;
}
int getSize(Stack S){
return S->len;
}
void push(ElementType X, Stack S){
Node *node = (Node *)malloc(sizeof(struct Node));
if(S&&node){
node->Element = X;
node->Next = S->header->Next;
S->header->Next = node;
S->len++;
}
}
void pop(Stack S){
if(!isEmpty(S)){
Node *node = S->header->Next;
S->header->Next = node->Next;
S->len--;
node->Next = NULL;
free(node);
}
}
ElementType top(Stack S){
return isEmpty(S) ? INT_MIN : S->header->Next->Element;
}
ElementType topAndPop(Stack S){
if(isEmpty(S))
return INT_MIN;
Node *node = S->header->Next;
S->header->Next = node->Next;
S->len--;
int res = node->Element;
free(node);
return res;
}
void makeEmpty(Stack S){
while(!isEmpty(S))
pop(S);
}
void disposeStack(Stack S){
makeEmpty(S);
free(S->header);
free(S);
}
void printStack(Stack S){
if(isEmpty(S))
printf("\Empty!");
else{
printf("\n%d(top)", S->header->Next->Element);
Node *node = S->header->Next->Next;
while(node){
printf("\n%d", node->Element);
node = node->Next;
}
}
}
- 测试
#include <stdio.h>
#include <stdlib.h>
#include "ListStack.h"
int main(int argc, char *argv[]) {
Stack s = createStack();
int i;
for(i = 0; i<10; i++)
push(i, s);
printStack(s);
printf("\n\n(%d)top , len=%d", top(s), getSize(s));
printf("\n(%d)topAndPop", topAndPop(s));
printf("\n(%d)top , len=%d\n", top(s), getSize(s));
printStack(s);
printf("\npop==>");
pop(s);
printf("\n(%d)top , len=%d\n", top(s), getSize(s));
printStack(s);
makeEmpty(s);
printf("\n\n(%d)top , len=%d", top(s), getSize(s));
return 0;
}
3-4 时间复杂度
栈的操作其实很简单,只能一头进出,所以相关的操作时间复杂度都是 O ( 1 ) O(1) O(1),且在我们的设计中,已经维护了一个元素个数计数器,那么getSzie()操作时间复杂度也是 O ( 1 ) O(1) O(1),这或许是很多教材上没有讨论Stack时间复杂度的原因,实在是太简单了。
3-5 Linux内核源码栈的寻迹
栈有很多经典的应用,如:linux内核的进程调度和虚拟地址空间,Android的ActivityStack和微信小程序的界面机制(界面的跳转,启动和恢复其实都是围绕着栈开展的)等等,再实际使用时并不一定使用了我们介绍的数据结构形式,但在核心思想上还是一致的:FILO。
单看C相关的实现:Linux进程描述符task_struct结构体和虚拟地址知识点篇幅都很大,不易展开讲解(建议闲的无聊的朋友自己去翻翻《Linux内核场景分析》以及相关的资料,参考链接[1]/[2]/[3])。可以先挑选了一个简单的分析对象Media-device和Media-entity(参考链接[4]/[5]/[6])。
3-5-1 Media模块
- Media framework简介(转至参考贴[4])
简单的说就是Linux在运行时状态下发现媒体设备(media device)拓扑并对其进行配置。为了达到这个目的,media framework将硬件设备抽象为一个个的entity,它们之间通过links连接。
(1)entity:硬件设备模块抽象(类比电路板上面的各个元器件、芯片)
(2)pad:硬件设备端口抽象(类比元器件、芯片上面的管脚)
(3)link:硬件设备的连线抽象,link的两端是pad(类比元器件管脚之间的连线) - Media-entity代码中stack的应用和体现
(1)这里的设备拓扑采用了图的形式去实现遍历(Media-entity.h)
struct media_entity_graph {
struct {
struct media_entity *entity;
int link;
} stack[MEDIA_ENTITY_ENUM_MAX_DEPTH];
int top;
};
(2)栈对应的操作和相关宏(Media-entity.c),push/pop/peek/top都有
/* push an entity to traversal stack */
static void stack_push(struct media_entity_graph *graph,
struct media_entity *entity)
{
if (graph->top == MEDIA_ENTITY_ENUM_MAX_DEPTH - 1) {
WARN_ON(1);
return;
}
graph->top++;
graph->stack[graph->top].link = 0;
graph->stack[graph->top].entity = entity;
}
static struct media_entity *stack_pop(struct media_entity_graph *graph)
{
struct media_entity *entity;
entity = graph->stack[graph->top].entity;
graph->top--;
return entity;
}
#define stack_peek(en) ((en)->stack[(en)->top - 1].entity)
#define link_top(en) ((en)->stack[(en)->top].link)
#define stack_top(en) ((en)->stack[(en)->top].entity)
(3)对应的调用举例
EXPORT_SYMBOL_GPL(media_entity_graph_walk_start)是对外保留的模块接口
/**
* media_entity_graph_walk_start - Start walking the media graph at a given entity
* @graph: Media graph structure that will be used to walk the graph
* @entity: Starting entity
*
* This function initializes the graph traversal structure to walk the entities
* graph starting at the given entity. The traversal structure must not be
* modified by the caller during graph traversal. When done the structure can
* safely be freed.
*/
void media_entity_graph_walk_start(struct media_entity_graph *graph,
struct media_entity *entity)
{
graph->top = 0;
graph->stack[graph->top].entity = NULL;
stack_push(graph, entity);
}
EXPORT_SYMBOL_GPL(media_entity_graph_walk_start);
3-5-2 Linux内核源码中的一段注释
实际上之前我们查看链表的linux源码中有这样一段描述,挺有意思的:
/*
* list_add - add a new entry
* @new: new entry to be added
* @head: list head to add it after
* Insert a new entry after the specified head.
* This is good for implementing stacks.
*/
Static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
“This is good for implementing stacks” 哈哈,其实在作者看来stack就是一个顺手用链表做出来的数据结构,并没有那么复杂,这同之前我们介绍的Media模块代码中使用数组简单实现stack功能的思想是一致的,够用就好。
3-5-3 Linux内核栈提要
此外,Linux内核在.s文件中有很多栈操作体现(push和pop),而在c代码中并没有那么直接,例如内核栈共用体 thread_union (include\linux\Sched.h)
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
里面就包含了一个内核栈结构,可以看到这里的栈是非常简单的,就一个long型数组,在具体应用中直接操作索引,所以栈的本质其实还是看栈标记物(SP-StackPointer,堆栈指针)+偏移,仅此而已:
extern union thread_union init_thread_union;
#define init_stack (init_thread_union.stack)
在 arch\tile\kernel\Setup.c 中:
/*
* per-CPU stack and boot info.
*/
DEFINE_PER_CPU(unsigned long, boot_sp) =
(unsigned long)init_stack + THREAD_SIZE;
在Process.h中就有SP相关代码:
#define INIT_SP (sizeof(init_stack) + (unsigned long) &init_stack)
#define INIT_THREAD { \
.sp = INIT_SP, \
}
[1] Linux进程描述符task_struct结构体详解–Linux进程的管理与调度(一) (这是一个系列,蛮好的)
[2] Linux虚拟地址空间布局
[3] linux 内核源代码情景分析——用户堆栈的扩展
[4] V4L2框架-media device(帮你了解Media模块)
[5] Media Controller devices (对应的英文文档)
[6] Linux的EXPORT_SYMBOL和EXPORT_SYMBOL_GPL的使用和区别 (帮助理解代码)