Data structure and algorithm practice: stack

Posted by evilgenius on Sun, 24 Oct 2021 01:43:38 +0200

Data structure and algorithm practice: stack

Arrays and pointers

Array name and first element address

Running the following code, we find that the array name and the first element address of the array are the same. Can we think that the array name is the first element address? In fact, it is not. Let's verify it through the following code:

#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;
}

If the first letter is the address of the first element, the value obtained by using sizeof() operator should be the size of a pointer, that is, the obtained value is 4, but the result is 20, which is the size of the entire arr array. Therefore, it can be proved that the array name is not the address of the first element, but why when printing the array name and the first address of the array name in the form of pointer variable, Are the two results the same? C99 standard is defined as follows:

In addition to initializing an array using the sizeof and & operators or using string literals, an expression containing an array name is converted to an expression containing a pointer to the first element and is not an lvalue after conversion. If the storage type of the array is a register, the behavior is undefined.

So we can understand the array name in this way: the array name is not the address of the first element, but the first element address is the value of the array name!

Accessing arrays with pointers

We know that an array is a continuous memory space opened up in memory, and the elements in the array are continuously distributed in memory. If you want to access an element in the array, you must know its address. After we declare the array, we use the operator [] + array subscript to access the array. What is the essence of []? Let's draw a conclusion first: [] is just an operator

‚Äč p[i]<==>*(p+i) i[p]<==>*(i+p)

Before understanding the use of pointer access, we must first understand another problem: define an array arr. As mentioned earlier, when evaluating arr, it is the first address of the first element of the array. What is arr+1? Is it an address plus a number 1? Or is there another understanding?

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;
}

According to the test, a+1 is not pointer a plus digit 1, and a+2 is not pointer a plus digit 2. In fact, the difference between their addresses is 4, and the pointer variable is exactly 4 bytes, that is, the difference between the two is exactly one pointer. Therefore, when we + i the pointer variable, we actually move the pointer pointing to the variable back i positions, so a[0]=*(a+0)=1,a[1]=*(a+1)=2... It can be seen that a+i is actually the address of the ith element of A.

Let's prove another conclusion I [P] < = = > * (I + P) in a piece of code

#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;
}

However, when accessing an array, we generally do not write it as 1[arr] with poor readability, but as arr[1].

Dynamic application array

malloc

Malloc() function: this function takes a parameter: the number of memory bytes required. Malloc() function will find an appropriate free memory block. Such memory is anonymous. That is, malloc() Allocates memory, but does not give it a name. However, it does return the first byte address of a dynamically allocated memory block. Therefore, you can assign this address to a pointer variable and use the pointer to access this memory. Because char represents 1 byte, malloc() The return type of is usually defined as a pointer to char. However, starting from the ANSI C standard, C uses a new type: a pointer to void. This type is equivalent to a * * general pointer * *. Malloc() Function can be used to return pointers to arrays, structures, etc., so usually the return value of the function will be cast to a matching type. In ANSI C, cast should be used to improve the readability of the code. If malloc() fails to allocate memory, null pointer will be returned.

Dynamic application array

Using our previous knowledge of arrays and pointers, we can apply for a continuous memory space and use a pointer variable (assumed to be p) to receive the address returned by the malloc() function. In this way, we can use it like an array and use p as an array name. That is, we can use the array in the way of P [subscript].

int main()
{
	int* p;
	int n = 5;//Define array length
	p = (int*)malloc(n * sizeof(int));//Memory size: array length × The number of bytes occupied by the element type of the array
	p[1] = 233;//Using arrays, assign values to array elements
	printf("p[i]=%d", p[1]);
	return 0;
}

free

**free() function: * * the parameter of this function is the address returned by malloc(), and this function releases malloc() Allocated memory. If the space pointed to by the parameter is not dynamically opened, the behavior of the free function is undefined. If the parameter is a NULL pointer, the function does nothing. A good habit is that the dynamically allocated memory is released in time after use.

Basic theory of stack

Definition of stack

Stack, also known as stack, is a linear table with limited operations. Insertion and deletion operations are only allowed at one end of the table. This end is called the top of the stack, and the other end is called the bottom of the stack. It follows the structure of first in first out, last in first out.

Stack operation

  • Init: initialize the stack according to the actual requirements
  • Push: put the new element on top of the top of the stack to make it a new top of the stack element
  • Pop: delete the top element of the stack and make its adjacent elements become new top elements
  • Get top element (top): get the top element of the stack without changing the stack

Stack operation implementation

There are two implementation types of stack: the chain stack with chain storage structure and the sequential stack with sequential storage structure. The chain stack can be easily implemented by using the single linked list with the leading pointer. Taking the position of the head pointer as the top of the stack can easily realize all operations of the stack. Moreover, we have focused on the analysis of the single linked list in the last blog, so we won't repeat it here , this time we choose to use array to realize sequential stack.

Define stack

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

We use the structure to define the stack, and do not specify the stack size first. If the stack size is defined during the definition, it may be different from our actual requirements when using the stack. Therefore, we hope to dynamically allocate the stack size as needed during stack initialization.

First, the pointer variable int* data is defined to receive the address of the array dynamically allocated when initializing the stack. I spent a lot of time explaining this. I won't repeat it here. Capacity is used to record the capacity of the current stack, and top is used to point to the position of the top element or the previous position of the top element. The initialization methods are different, and the pointing positions are different.

Stack initialization

First define a stack struct Stack st, and give the call method init (& St, 5); when passing parameters, we choose to pass the address of St, because we want to change the contents of the stack during initialization (dynamically allocate the array, the number of elements of the stack, and the pointing position of the elements at the top of the stack)

Operation implementation

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

Initializing top to - 1 means that top points to the highest element. Initializing top to 0 means that top points to the space above the highest element. There is no difference between the two, but the conditions are different when judging whether the stack is empty or full.

Judge stack empty

Calling method: isEmpty(ps). Because the operation of judging stack emptiness is often called by other operations, and the parameters received by other operations are pointers defining the stack, we also choose to receive pointers when implementing the operation of judging stack emptiness, but we don't want to modify the stack when we perform empty judgment, so we use const to modify it, And the operation needs to judge whether the stack is empty or non empty, so there is a return value. Therefore, we define the function as int type. If the stack is empty, it returns 1 and if the stack is non empty, it returns 0.

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

//When initializing the stack, top is set to 0, so when top=0, the stack is empty. For conditional expressions, when the expression is true, the value of the expression is 1, and when the expression is false, the value of the expression is 0, which coincides with our function design idea, so we directly implement return PS - > Top = = 0

Judge stack full

Call method: isFull(ps). The design idea of the function to judge the stack full is basically the same as that of the empty operation, but the judgment conditions are different. When we initialize the stack, top points to the previous position of the top element of the stack. Therefore, when the value of top is equal to the capacity of the stack, the stack is full. For example, when the stack capacity is 1, top is initialized to 0. After inserting an element, top is 1. At this time, the stack is full, and the value of top is exactly the same as capacity.

Operation implementation

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

Push

Function calling method: push (& St, 11);, Pass in the pointer of the stack we want to operate on and the element to be put into the stack; Our operation may fail or succeed, so we design the return value of the stack function as int, return 1 for stack success and 0 for stack failure

Operation implementation

int push(struct Stack* ps,int x)
{
    if(isFull(ps))
    {
        return 0;//Stack full, failed to stack, return 0
    }
    else
    {
        ps->data[ps->top]=x;
        ps->top++;
        return 1;
    }
}

When entering the stack, we must first judge whether the stack is full. If the stack is full, the operation is illegal and returns 0 to end. If the stack is not full, PS - > data [PS - > Top] = X. when defining, we point top to the previous position of the top element of the stack. Therefore, when entering the stack, we can directly select the subscript of the insertion position as top. After the insertion operation is completed, top + +, the operation is successful and returns 1 to end the operation.

Out of stack

Function calling method: Pop (& St, & x). First, pass in the address of the stack we want to operate and the address of the variable we defined before the operation, assuming x, which is used to record what the elements we put into the stack are. Our operation may fail or succeed. Therefore, we design the return value of the out of stack function as int, and return 1 after successful out of the stack, Stack out failed, return 0

Operation implementation

int pop(struct Stack* ps,int* px)
{
    if(isEmpty(ps))
    {
        return 0;//Stack empty, stack out operation illegal, return 0
    }
    else
    {
        ps->top--;//Move down to the top of the stack element
        *px = ps->data[ps->top];//Returns the top of stack element to the incoming x
        return 1;
        
    }
}

When we define the stack, we define that top points to the previous position of the top element in the design, so when we top -- it indicates that the top element of the stack has been changed to the element adjacent to the original top element. In fact, the out of stack operation has been completed. At the same time, it indicates that there are no elements in the space where the original top element is located, At any time, the element can be re inserted into the stack at that position.

Get stack top element

Function calling method: top (& St, & x), first pass in the address of the stack we want to operate, and then pass in the address of the variable we defined to record the value of the top element of the stack before taking the top element of the stack. For example, x, our operation may fail or succeed, so we design the return value of the function taking the top element of the stack as int type, and return 1 if the operation is successful, The operation failed and returned 0

int top(const struct Stack* ps, int* px)
{
	if (isEmpty(ps))
	{
		return 0;//The stack is empty, the operation of fetching the top element of the stack is illegal, and 0 is returned
	}
	else
	{
		*px = ps->data[ps->top-1];//The position of the stack top element is the next position of the top position
		return 1;//Indicates that the operation of fetching the top element of the stack is successful
	}
}

*PX = PS - > data [PS - > top-1], because our top is the last position of the top element of the stack, when we go to the top element of the stack, we go to the top-1 element.

Destroy stack

Although all the space occupied by the program will be released after the end of the program, sometimes the stack needs to be used repeatedly and initialized repeatedly. In the middle of the program running, you should do destroy() to avoid the possible memory leakage caused by long-term running and repeated calls of the program. It is a good habit to release the dynamically allocated memory in time after use

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

Effect test

Complete test code

#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;//Stack full, failed to stack, return 0
    }
    else
    {
        ps->data[ps->top]=x;
        ps->top++;
        return 1;
    }
}

int pop(struct Stack* ps,int* px)
{
    if(isEmpty(ps))
    {
        return 0;//Stack empty, stack out operation illegal, return 0
    }
    else
    {
        ps->top--;//Move down to the top of the stack element
        *px = ps->data[ps->top];//Returns the top of stack element to the incoming x
        return 1;
        
    }
}


int top(const struct Stack* ps, int* px)
{
	if (isEmpty(ps))
	{
		return 0;//The stack is empty, the operation of fetching the top element of the stack is illegal, and 0 is returned
	}
	else
	{
		*px = ps->data[ps->top-1];//The position of the stack top element is the next position of the top position
		return 1;//Indicates that the operation of fetching the top element of the stack is successful
	}
}


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("Pop up stack top element%d\n", x);
	top(&st, &x);
	printf("Now the top element of the stack is%d\n", x);
	pop(&st, &x);
	printf("Pop up stack top element%d\n", x);
	top(&st, &x);
	printf("Now the top element of the stack is%d\n", x);
	push(&st, 2333);
	printf("Push \n");
	top(&st, &x);
	printf("After entering the stack,Stack top element is%d\n",x);
	destroy(&st);
	return 0;
}

This paper spent a lot of space. The array began to talk, interspersed with the content of dynamically allocating the array, paving the way for the implementation of our stack. I hope readers can appreciate the author's good intentions and serious understanding. At the same time, due to the author's limited level, there may be some errors in the article. I hope you can criticize and correct more!