双12买了本《剑指offer》,里面好多题目之前都做过,但仅限于做出来,当时没有思考太多,很多都是直接调类(用的java)。今天开始重新学习,想着用C再实现一遍,这次要考虑更深层的东西,比如时间复杂度这些。
言归正传,再次学习还是从链表开始,第一道关于链表的题目是从尾到头打印链表。看到这道题,我第一反应想的是:把链表反转,然后再依次输出每个结点的值就好了,正好还有一道链表反转的题,一次能完成两个呢。看了作者的书,原来反转之后链表的结构就变了,如果面试官要求不能反转链表,这个解法就是不正确的。
因此这道题目的思路应该有三个:
- 反转链表,依次输出节点上的值;
- 利用栈先进后出的特点,将链表中的值依次入栈,再依次出栈;
- 递归。
由于反转链表在另一道题目当中,因此这里只考虑后两种情况。
利用栈从尾到头打印链表
首先我得定义链表的结构,这个太熟了,我几乎想也不想就写出了下面的代码:
typedef struct Linknode
{
int val; //存放数据
Linknode* next; //存放指针
}Linknode;
然后,后面的运行中我就遇到了大麻烦,十几个错误都是Linknode未定义。查看了很多资料才知道,定义中的next不能只用Linknode,还需要加上struct。好吧,脑子熟和手熟是两回事,我还是得多练习。
所以,正确的定义应该是:
typedef struct Linknode
{
int val; //存放数据
struct Linknode* next; //存放指针
}Linknode;
定义好之后,我准备用C语言写这个逆转函数。参考了剑指offer,很简洁很清晰。
但是,书上是用C++实现的。我最近java和C混着学已经要晕,现在再学一个C++(虽然大学学过,但后来就没有再用)怕是要赶不上春招。。。emmm要哭了。。。我为什么这么菜!!!
所以我打算用C实现(java会让我忽略很多底层的东西)。然后我就发现,C里面没有定义栈啊!!好吧,既然都决定了,就硬着头皮上吧。本题目用到的是创建栈、入栈、出栈、判断栈空、判断栈满、查找栈顶操作,下面依次定义。
栈的定义
typedef struct Stack
{
int* arr; //存放栈的首地址
int len; //栈的长度
int top; //栈顶的下标
}Stack;
创建栈
Stack* create_stack(int len)
{
Stack* stack = malloc(sizeof(Stack));
stack->arr = malloc(siezof(int)*len);
stack->len = len;
stack->top = -1;
return stack;
}
入栈
bool push_stack(Stack* stack,int val)
{
if(full_stack(stack))
return false;
stack->arr[++stack->top] = val;
return true;
}
判断栈是否已满
bool full_stack(Stack* stack)
{
return stack->top + 1 >= stack->len;
}
出栈
bool pop_stack(Stack* stack)
{
if(empty_stack(stack))
return false;
stack->top--;
return true;
}
判断栈是否为空
bool empty_stack(Stack* stack)
{
return stack->top == -1;
}
查找栈顶
int top_stack(Stack* stack)
{
if(!empty_stack(stack))
return NULL;
return stack->arr[stack->top];
}
注意:这些函数都是我从CSDN前辈那里学习的,查找栈顶函数原来的返回值是:
return stack->arr + stack->top;
我在后面调试的过程中发现打印出的全都是各个结点的地址,因此做了点改动。
至此,准备工作就结束了。开始写逆转函数:
void PrintListReversingly_iteratively(Listnode* phead)
{
if(phead == NULL)
return;
int temp;
Stack* stack = create_stack(10);
while(phead != NULL)
{
push_stack(stack,phead->val);
phead = phead->next;
}
while(!empty_stack(stack))
{
temp = top_stack(stack);
printf("%d ",temp);
pop_stack(stack);
}
printf("\n");
}
接下来,我们要写主函数来调用这个函数:
int main(void)
{
//创建单链表
Listnode* head = (Listnode*)malloc(sizeof(Listnode));
head->next = NULL;
for(int i = 0;i < 7;i++)
{
Listnode* s= (Listnode*)malloc(sizeof(Listnode));
s->val = i + 1;
s->next = h-> next;
h->next = s;
}
PrintListReversingly_iteratively(head);
return 0;
}
运行一下:
看到输出结果,我困惑了,难道输出结果不应该是 7,6,5,4,3,2,1 ???哪里又错了,还有,最后那堆数字是什么鬼???
这时,我用单步调试看了一下调用PrintListReversingly_iteratively前链表的结构:
这样子不像有错误呀,我把head展开:
显然,我第一个插进去的数值1在链表末尾,最后插入的数值7却在头指针后面,而头指针是一串乱数字。
这时候,大学数据结构老师的声音在我耳边回响:头插法和尾插法,要考的!!!还好我认真学了,返回主函数,我构造链表的方法确实是头插法。至于头节点的数据,创建链表的过程时我并未给它赋值,所以随机赋了一个值,入栈的时候这个值也参与了(以后给头节点赋初值NULL即可)。
知道了问题所在,接下来修改代码:
int main(void)
{
//创建单链表
Listnode* head = (Listnode*)malloc(sizeof(Listnode));
Listnode* end;
head->val = NULL;
head->next = NULL;
end = head;
for(int i = 0;i < 7;i++)
{
Listnode* s= (Listnode*)malloc(sizeof(Listnode));
s->val = i + 1;
//尾插法
end->next = s;
end = s;
}
PrintListReversingly_iteratively(head);
return 0;
}
运行一下:
什么也没有输出!!
在逆转函数前设置断点可以看到:
用尾插法之后链表的值为0,1,2,3,4,5,6,7,说明尾插法建立单链表成功。
这两幅图片不一样的地方为链表结尾,头插法中,链表结尾的next为NULL,而尾插法中,链表结尾的next无值。从断点开始单步调试至val为7时弹出错误:
说明确实是尾节点的next有问题。
所以在调用逆转函数之前,给end的next赋值NULL。调试一下,成功!!
所以最终的主函数为:
int main(void)
{
//创建单链表
Listnode* head = (Listnode*)malloc(sizeof(Listnode));
Listnode* end;
head->val = NULL;
head->next = NULL;
end = head;
for(int i = 0;i < 7;i++)
{
Listnode* s= (Listnode*)malloc(sizeof(Listnode));
s->val = i + 1;
//尾插法
end->next = s;
end = s;
}
//不加该句会出错!!!!
end->next = NULL;
PrintListReversingly_iteratively(head);
return 0;
}
递归实现从尾到头打印链表
仅需改变逆转函数即可,递归就是通过缩小问题的规模不断调用自己的过程,好理解,代码也简单。
void PrintListReversingly_Recursively(ListNode* phead)
{
if(phead == NULL)
return;
if(phead->next != NULL)
PrintListReversingly_Recursively(phead->next);
printf("%d ",phead->val);
}
在主函数中添加调用语句:
PrintListReversingly_Recursively(head);
输出结果为:
OK !!!
总结一下,今天完成了用C语言从尾到头打印链表,其中包括链表和栈的定义,栈中常用方法的定义,复现了我今天遇到了错误以及解决方法,收获满满~