数据结构复习-线性结构(链表,栈,链栈,字符串)
最近有chatgpt还是很爽的,之前想重新写一遍基本的数据结构和算法,苦于懒得自己敲模板,懒得自己编测试用例, 没想到现在有了chatgpt,各种测试用例和模板都可以自动生成了,尤其是测试, 可以让chatgot生成很严苛很变态的测试, 总能发现些问题,还是很爽, 非常的爽,于是打算重新手写一下数据结构和算法那些. 也是怕到时候笔试面试的时候太紧张了或者忘太多了写不出来, 到时候解释说我其实真的会, 我估计面试官肯定不信哈哈哈哈哈哈哈.
其实类似于这样的线性结构功能还是很强大的, 在xv6(一个教学用途的操作系统)的代码里多半是通过这种linklist管理进程,内存等等系统资源, 之前搞数学建模的时候通过for加上list能把简单重复的一些工作抽象到高一个的层次, 比如自动画图啥的, 换句话说就是我只要画一个图就能得到这种类型的所有图, 只要通过list遍历剩下的就行了, 总的来说算是性价比非常高的数据结构了, 而且对新手比较友好, 理解了LinkList,LinkStack这些能更方便理解剩下的指针型的数据结构.
LinkList
重新写一遍还是有收获的, 碰到了很多指针相关的问题, 有时候一个指针不够用还是需要两个指针跟着跑, 让一个指针先走一步,这样就很方便.
还有就是野指针的问题, free之后指向这块内存的指针实际上是被填充了别的东西, 并不是也变成了NULL,需要手动把指针设置成NULL以避免出错.
这个时候应该还有一个问题, 学了操作系统之后知道在多线程模式下这个链表其实是不安全的, 最不安全的情况莫过于一个程序用完了这个链表,并且试图释放这个链表, 其它用这个链表的进程估计就出大bug了, 所以以后用的时候尽量不那么用, 不然再维护一个相关的锁再加上啥引用计数啥的也不是那么方便.
重新写了一遍之后发现, 第一遍学的时候因为语法问题, 加上本身想的没那么透彻, 其实是很混乱的一个状态, 还是有收获的.
#define MYDEBUG
#if defined(MYDEBUG)
#include <assert.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#define DataType int
typedef struct ListNode {
DataType val; // 节点的值
struct ListNode* next; // 指向下一个节点的指针
} ListNode;
// 初始化链表,返回头节点
ListNode* createList();
// 在链表尾部插入一个节点
int insert(ListNode* head, DataType val);
// 在指定位置插入一个节点
int insertAt(ListNode* head, DataType val, int pos);
// 删除链表中第一个指定值的节点
int remove(ListNode* head, DataType val);
// 删除链表中指定位置的节点
int removeAt(ListNode* head, int pos);
// 输出链表中的所有元素
void printList(ListNode* head);
void freeList(ListNode* head);
//#define MYDEBUG
//#if defined(MYDEBUG)
//#define ASSERT(expression) assert(expression)
//#else
//#define ASSERT(expression) ((void)0)
//#endif
//
//int main() {
// // 创建一个空链表
// ListNode* head = createList();
//
// // 在链表尾部插入几个节点
// insert(head, 1);
// insert(head, 2);
// insert(head, 3);
// ASSERT(head->val == 3);
// ASSERT(head->next->val == 1);
// ASSERT(head->next->next->val == 2);
// ASSERT(head->next->next->next->val == 3);
printList(head);
// insertAt(head, 4, 2);
printList(head);
// ASSERT(head->val == 4);
// ASSERT(head->next->val == 1);
// ASSERT(head->next->next->val == 2);
// ASSERT(head->next->next->next->val == 4);
// ASSERT(head->next->next->next->next->val == 3);
//
// remove(head, 2);
// ASSERT(head->val == 3);
// ASSERT(head->next->val == 1);
// ASSERT(head->next->next->val == 4);
// ASSERT(head->next->next->next->val == 3);
//
// // 删除链表中指定位置的节点
// removeAt(head, 1);
// ASSERT(head->val == 2);
// ASSERT(head->next->val == 1);
// ASSERT(head->next->next->val == 3);
//
// freeList(head);
// head=NULL;
// ASSERT(head == NULL);
//
// return 0;
//}
// 初始化链表,返回头节点
ListNode* createList() {
// TODO
ListNode *head=(ListNode*)(malloc(sizeof(ListNode)));
head->next=NULL;
head->val=0;
return head;
}
// 在链表尾部插入一个节点
int insert(ListNode* head, DataType val) {
// TODO
ListNode *node=(ListNode*)(malloc(sizeof(ListNode)));
if(!node) return -1;
head->val++;
while (head->next){head=head->next;}
node->next=NULL;
node->val=val;
head->next= node;
return 0;
}
// 在指定位置插入一个节点
int insertAt(ListNode* head, DataType val, int pos) {
ListNode *last=head;
head->val++;
head=head->next;
while (pos--){
head=head->next;
last=last->next;
}
ListNode *new_node=createList();
if(!new_node) return -1;
last->next=new_node;
new_node->val=val;
new_node->next=head;
return 1;
}
int free_node(ListNode *node){
free(node);
node=NULL;
return 1;
}
// 删除链表中第一个指定值的节点
int remove(ListNode* head, DataType val) {
head->val--;
ListNode *last=head;
head=head->next;
while (head){
if(head->val==val){
last->next=head->next;
free_node(head);
return 1;
}
last=last->next;
head=head->next;
}
return -1;
}
// 删除链表中指定位置的节点
int removeAt(ListNode* head, int pos) {
ListNode* last=head;
ListNode* head1=head;
head=head->next;
while (pos--){
head=head->next;
last=last->next;
if(!head) {return -1;
printf("有问题\n");}
}
head1->val--;
last->next=head->next;
free_node(head);
return 1;
}
// 输出链表中的所有元素
void printList(ListNode* head) {
if(!head) return;
printf("size %d ;",head->val);
head=head->next;
while (head){
printf("%d ",head->val);
head=head->next;
}
printf("\n");
}
// 释放链表中的所有元素
void freeList(ListNode* head) {
ListNode *last=head;
while(last){
head=last;
last=last->next;
free(head);
head=NULL;
}
}
ArrayStack
相较于LinkList来说, ArrayStack就相对容易些了, 只是要注意top的边界问题, 一开始想不出来为什么top的初始值是-1,
栈顶指针
top
的初始值为-1
是因为栈顶指针的定义是指向栈顶元素上面的一个位置,而在栈为空的情况下,栈顶指针应该指向一个不存在的位置,所以将栈顶指针的初始值设为-1
,表示栈中没有元素
然后就是要稍微绕一下,因为从零开始数会少数一个,从-1开始估计是少数两个, push因为-1的原因要先加再填,pop就是先出再减了.
#include <stdio.h>
#include "stdio.h"
#define MAX_STACK_SIZE 100
typedef int ElementType;
typedef struct {
ElementType data[MAX_STACK_SIZE];
int top;
} Stack;
// 初始化栈
void initStack(Stack* s);
// 判断栈是否为空
int isEmpty(Stack* s);
// 判断栈是否已满
int isFull(Stack* s);
// 入栈操作
void push(Stack* s, ElementType val);
// 出栈操作
ElementType pop(Stack* s);
// 获取栈顶元素
ElementType top(Stack* s);
// 打印栈中所有元素
void printStack(Stack* s);
void testStack();
// 主函数,用于测试栈的各个功能
int main() {
Stack s;
initStack(&s);
// 测试入栈操作
push(&s, 1);
push(&s, 2);
push(&s, 3);
// 测试获取栈顶元素
printf("top: %d\n", top(&s));
// 测试打印栈中所有元素
printStack(&s);
// 测试出栈操作
ElementType val = pop(&s);
printf("pop: %d\n", val);
printStack(&s);
val = pop(&s);
printf("pop: %d\n", val);
printStack(&s);
val = pop(&s);
printf("pop: %d\n", val);
printStack(&s);
// 测试判断栈是否为空
if (isEmpty(&s)) {
printf("Stack is empty\n");
} else {
printf("Stack is not empty\n");
}
// 测试判断栈是否已满
if (isFull(&s)) {
printf("Stack is full\n");
} else {
printf("Stack is not full\n");
}
testStack();
return 0;
}
void initStack(Stack* s) {
// TODO: 实现初始化栈的代码
s->top=-1;
}
int isEmpty(Stack* s) {
// TODO: 实现判断栈是否为空的代码
return s->top==-1;
}
int isFull(Stack* s) {
// TODO: 实现判断栈是否已满的代码
return s->top>=MAX_STACK_SIZE-1;
}
void push(Stack* s, ElementType val) {
// TODO: 实现入栈操作的代码
if(!isFull(s))
s->data[++s->top]=val;
}
ElementType pop(Stack* s) {
// TODO: 实现出栈操作的代码
if(!isEmpty(s))
return s->data[s->top--];
return -1;
}
ElementType top(Stack* s) {
// TODO: 实现获取栈顶元素的代码
if(!isEmpty(s))
return s->data[s->top];
return -1;
}
void printStack(Stack* s) {
printf("stack size: %d, ",s->top+1);
for(int i=s->top-1;i>=0;i--)
printf("%d ",s->data[i]);
// TODO: 实现打印栈中所有元素的代码
}
#include <assert.h>
void testStack() {
Stack s;
initStack(&s);
// Test isEmpty and isFull functions
assert(isEmpty(&s));
assert(!isFull(&s));
// Test push function
for (int i = 1; i <= MAX_STACK_SIZE; i++) {
push(&s, i);
assert(s.top == i-1);
}
assert(isFull(&s));
assert(!isEmpty(&s));
// Test push function on full stack
push(&s, MAX_STACK_SIZE + 1);
assert(s.top == MAX_STACK_SIZE - 1);
// Test pop function
for (int i = MAX_STACK_SIZE; i >= 1; i--) {
int val = pop(&s);
assert(val == i);
assert(s.top == i-2);
}
assert(isEmpty(&s));
assert(!isFull(&s));
// Test pop function on empty stack
int val = pop(&s);
assert(val == -1);
// Test top function
for (int i = 1; i <= MAX_STACK_SIZE; i++) {
push(&s, i);
assert(top(&s) == i);
}
assert(top(&s) == MAX_STACK_SIZE);
// Test top function on empty stack
initStack(&s);
assert(top(&s) == -1);
// Test printStack function
printStack(&s);
for (int i = 1; i <= MAX_STACK_SIZE; i++) {
push(&s, i);
}
printStack(&s);
for (int i = MAX_STACK_SIZE; i >= 1; i--) {
pop(&s);
}
printStack(&s);
}
LinkStack
在链栈中,next是指向下一个节点的指针,指向栈顶元素下面的节点,也就是离栈顶最远的节点。因此,next靠近栈底部。空的链栈让top指向空即可. 专门让ChatGPT生成了很严苛的测试, 不够它生成出来的测试有个别地方是有问题的, 也不能全相信它.
这里发现了对C语言有点迷糊的地方, 使用LinkList的时候要注意一下指针的作用范围, 如果一个指针指向的不是一块单纯只放数据的内存,而是另外一个指针所在的内存,
如下:
typedef int ElementType;
typedef struct Node {
ElementType data;
struct Node* next;
} Node;
typedef struct {
Node* top;
} LinkStack;
//方法1
void printStack(LinkStack* s) {
// TODO: 实现打印链栈中所有元素的代码
printf("link_stack: ");
LinkStack *p=s;
while (p->top){
printf("%d ",p->top->data);
p->top=p->top->next;
}
}
//方法2
void printStack(LinkStack* s) {
// TODO: 实现打印链栈中所有元素的代码
printf("link_stack: ");
Node *p=s->top;
while (p){
printf("%d ",p->data);
p=p->next;
}
}
这里方法1就是让p指向了s指向的内存, p和s都是指向相同的内存
p->top 就是 (*p).top 也就是访问了s指向的那块内存, 而s那块内存存的是指针, p->top=p->next就改变了这块内存的值, 从而改变了栈顶的位置,无意间破坏了栈的结构.
方法2没有破坏栈的结构是因为p是指针,p=p->next只是将指针向下移,
可视化一下是这样
s{top}----> node{data,next}------------------------------->node1{data,next}
p=s p↑ --> p->top=p->top->next -----> p->top↑
方法二大概是这样:
node1(地址99):{value:2,next=100} ;node2(地址100):{value:2,next=101}
先p->node1; 指针指向node1, p=p->next 就是把指针挪到了next里写的node2的地址上.
还好现在不是当时初学c的时候了, 当时碰到这情况真得卡好几个小时想不明白哈哈哈哈.
总的代码带测试的:
#define MAX_STACK_SIZE 4096
typedef int ElementType;
typedef struct Node {
ElementType data;
struct Node* next;
} Node;
typedef struct {
Node* top;
} LinkStack;
// 初始化链栈
void initStack(LinkStack* s);
// 判断链栈是否为空
int isEmpty(LinkStack* s);
// 入栈操作
void push(LinkStack* s, ElementType val);
// 出栈操作
ElementType pop(LinkStack* s);
// 获取栈顶元素
ElementType top(LinkStack* s);
// 打印链栈中所有元素
void printStack(LinkStack* s);
#include <stdio.h>
#include <stdlib.h>
#include <cassert>
void initStack(LinkStack* s) {
// TODO: 实现初始化链栈的代码
s->top=NULL;
}
int isEmpty(LinkStack* s) {
// TODO: 实现判断链栈是否为空的代码
return s->top==NULL;
}
void push(LinkStack* s, ElementType val) {
// TODO: 实现入栈操作的代码
if(!s)
return ;
Node* node= (Node*) malloc(sizeof(Node));
node->next=s->top;
s->top=node;
node->data=val;
}
ElementType pop(LinkStack* s) {
// TODO: 实现出栈操作的代码
if(isEmpty(s))
return NULL;
ElementType element=s->top->data;
Node *f=s->top;
s->top=s->top->next;
free(f);
return element;
}
ElementType top(LinkStack* s) {
// TODO: 实现获取栈顶元素的代码
if(isEmpty(s)) return NULL;
return s->top->data;
}
void freeStack(LinkStack *s){
LinkStack *p=s;
while (p->top){
s->top=p->top;
p->top=p->top->next;
free(s->top);
free(s);
}
}
void printStack(LinkStack* s) {
// TODO: 实现打印链栈中所有元素的代码
printf("link_stack: ");
Node *p=s->top;
while (p){
printf("%d ",p->data);
p=p->next;
}
}
int main() {
LinkStack s;
initStack(&s);
// 测试初始状态为空栈
assert(isEmpty(&s));
// 测试入栈操作
push(&s, 1);
push(&s, 2);
push(&s, 3);
// 测试获取栈顶元素
assert(top(&s) == 3);
// 测试打印链栈中所有元素
printStack(&s);
printf("\n");
// 测试出栈操作
ElementType val = pop(&s);
assert(val == 3);
printStack(&s);
printf("\n");
val = pop(&s);
assert(val == 2);
printStack(&s);
printf("\n");
val = pop(&s);
assert(val == 1);
printStack(&s);
printf("\n");
// 测试判断链栈是否为空
assert(isEmpty(&s));
// 测试入栈操作
push(&s, 4);
push(&s, 5);
push(&s, 6);
// 测试获取栈顶元素
assert(top(&s) == 6);
// 测试打印链栈中所有元素
printStack(&s);
printf("\n");
// 测试出栈操作
val = pop(&s);
assert(val == 6);
printStack(&s);
printf("\n");
val = pop(&s);
assert(val == 5);
printStack(&s);
printf("\n");
val = pop(&s);
assert(val == 4);
printStack(&s);
printf("\n");
// 测试判断链栈是否为空
assert(isEmpty(&s));
// 测试连续入栈和出栈
push(&s, 1);
push(&s, 2);
push(&s, 3);
push(&s, 4);
push(&s, 5);
val = pop(&s);
assert(val == 5);
push(&s, 6);
// 测试获取栈顶元素
assert(top(&s) == 6);
// 测试打印链栈中所有元素
printStack(&s);
printf("\n");
// 测试出栈操作
val = pop(&s);
assert(val == 6);
printStack(&s);
printf("\n");
val = pop(&s);
assert(val == 4);
printStack(&s);
printf("\n");
val = pop(&s);
printf("pop: %d\n", val);
assert(val == 3); // 断言
printStack(&s);
val = pop(&s);
printf("pop: %d\n", val);
assert(val == 2); // 断言
printStack(&s);
val = pop(&s);
printf("pop: %d\n", val);
assert(val == 1); // 断言
printStack(&s);
// 测试判断栈是否为空
if (isEmpty(&s)) {
printf("Stack is empty\n");
assert(s.top == NULL); // 断言
} else {
printf("Stack is not empty\n");
}
// 测试出栈空栈
val = pop(&s);
assert(val == NULL); // 断言
// 测试获取空栈的栈顶元素
val = top(&s);
assert(val == NULL); // 断言
// 测试判断链栈是否为空
if (isEmpty(&s)) {
printf("LinkStack is empty\n");
assert(s.top == NULL); // 断言
} else {
printf("LinkStack is not empty\n");
}
// 测试链栈出栈空栈
val = pop(&s);
assert(val == NULL); // 断言
// 测试链栈获取空栈的栈顶元素
val = top(&s);
assert(val == NULL); // 断言
// 测试链栈入栈空指针
push(NULL, 1);
assert(s.top == NULL); // 断言
// 释放链栈
freeStack(&s);
return 0;
}
String
在C程序设计语言
(黑皮书的那个)里有很多关于字符串的示例和练习, 还记得当时是做了几十到题之后熟悉了c语言的指针, 梦回当初了属于是, 时间长了手法有点生疏了, 还是要开调试. 测试用例也是ChatGPT
生成的.
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
static int strlen(char *s){
char *a=s;
while (*(++s));
return s-a;
}
char* createString(int len);
char* copyString(char* s);
char* concatenateStrings(char* s1, char* s2);
int compareStrings(char* s1, char* s2);
char* findSubstring(char* s, char* sub);
int getLength(char* s);
void reverseString(char* s);
char* createString(int len){
char *s= (char *)malloc(sizeof(char )*(len+1));
/*为了测试顺便随便塞点东西进去*/
memset((void *)s,'0',sizeof(char )*len);
memset((void *)(s+len),0,sizeof(char ));
return s;
}
char* copyString(char* s){
char *first=s;
char *r,*p;
while (*s++)
;
/* *s=\0 */
r= (char*)malloc(sizeof(char )*(s-first+1));
p=r;
while (first!=s)
*(p++)=*(first++);
*p=0;
return r;
}
char* concatenateStrings(char* s1, char* s2){
int len= strlen(s1)+ strlen(s2);
char *r,*p;
r= (char *)malloc(sizeof(char )*(len+1));
p=r;
while (*s1++)
*p++=*s1;
while (*s2++)
*p++=*s2;
return r;
}
int compareStrings(char* s1, char* s2){
while ((*s1++==*s2++)&&(*s1)&&(*s2));
if((*s1==*s2)&&(*s1==0)) return 1;
return 0;
}
char* findSubstring(char* s, char* sub){
int len_sub= strlen(sub);
int len_s= strlen(s);
char *r=(char *)calloc(len_sub+1, sizeof(char ));
char *p=s;
while (p+len_sub<s+len_s){
memcpy(r,p,len_sub*(sizeof(char )));
if(compareStrings(r,sub))
return r;
p++;
}
return NULL;
}
int getLength(char* s){
char *a=s;
while (*(++s));
return s-a;
}
void reverseString(char* s){
char *s1=s+ strlen(s)-1;
while (s+1<s1){
char temp=*s1;
*s1=*s;
*s=temp;
s++,s1--;
}
}
int main() {
// 测试创建字符串函数
char *s1 = createString(10);
printf("createString: %s\n", s1);
free(s1);
// 测试复制字符串函数
char *s2 = "hello";
printf("strlen: %d\n", strlen(s2));
char *copy = copyString(s2);
printf("copyString: %s\n", copy);
free(copy);
// 测试拼接字符串函数
char *s3 = "world";
char *result = concatenateStrings(s2, s3);
printf("concatenateStrings: %s\n", result);
free(result);
// 测试比较字符串函数
char *s4 = "hello";
char *s5 = "world";
int cmp1 = compareStrings(s2, s4);
int cmp2 = compareStrings(s2, s5);
printf("compareStrings: %d %d\n", cmp1, cmp2);
// 测试查找子字符串函数
char *s6 = "hello world";
char *sub1 = "wor";
char *sub2 = "abc";
char *find1 = findSubstring(s6, sub1);
char *find2 = findSubstring(s6, sub2);
printf("findSubstring: %s %s\n", find1, find2);
//
// 测试获取字符串长度函数
int len1 = getLength(s2);
int len2 = getLength("");
printf("getLength: %d %d\n", len1, len2);
//
// 测试反转字符串函数
char s7[] = "abcdef";
reverseString(s7);
printf("reverseString: %s\n", s7);
return 0;
}