数据结构之链表面试题
1.前言
关于链表的考察和应用始终是公司考察的重点,因为链表可以衍生出更多具有特性的数据结构(链式栈、队列),那么我们在编写链表这类数据结构的时候一定要设计好合理的结构体信息,结构体的设计越丰富合理,越有利于程序的编写。今天我们将采用带有控制信息的链表为例,为大家讲解各大公司面试题对链表的考察。
面试题主要涉及一下部分:
1.链表初始化
2.链表的销毁
3.头部插入
4.尾部插入
5.头部删除
6.尾部删除
7.显示链表信息
8.升序排列链表
9.降序排列链表
10.得到链表的长度
11.合并两个已经排序的链表
12.合并两个已经排序的链表(递归方式)
13.得到链表中间节点的位置
14.得到链表的倒数第k个节点
15.反转链表
16.链表的拷贝
17.逆序打印链表
18.判断两个链表是否有交点
19.找到两个链表的第一个交点
20.在O(1)时间复杂度删除链表节点
21.判断链表是否有环
22.判断链表是否有环
23.找到链表环的入口(简便)
24.找到链表环的入口
链表面试题实现
关于上述所有的问题我们首先引入list.h文件,这个头文件中我们列举出了带有控制信息的链表节点的设计,分为控制信息和节点信息两部分,两者的定义如下所示:
#define ZERO (0)
#define ONLY_ONE (1)
#define TWO (2)
#define FALSE (0)
#define TRUE (1)
//带有控制信息的单链表
typedef unsigned char Boolean;
//链表节点信息
typedef struct List_node
{
int data; //数据区域
struct List_node *next; //链域
}List_node;
//链表控制信息
typedef struct List
{
struct List_node *head; //链表头部节点
struct List_node *tail; //链表尾部节点
int count; //链表节点个数
}List;
关于两者之间的关系如下图所示:
图1
控制信息可以清楚的知道链表当前的状态,对于链表头和尾部的查找或者说取得链表中的元素个数都可以在O(1)时间复杂度完成。在介绍完基本情况后接下来处理链表的接口问题:
//链表操作接口
List *init_list(void) ; //链表初始化
void destroy_list(List **list) ; //链表的销毁
Boolean push_front(List *list, int value) ; //头部插入
Boolean push_back(List *list, int value) ; //尾部插入
Boolean pop_front(List *list) ; //头部删除
Boolean pop_back(List *list) ; //尾部删除
void show_list(List *list) ; //显示链表信息
void sort_list_ascend(List *list) ; //升序排列链表
void sort_list_descend(List *list) ; //降序排列链表
int get_list_count(List *list) ; //得到链表的长度
///
List *merge_two_lists(List *list1, List *list2) ; //合并两个已经排序的链表
List *merge_two_lists_recure(List *list1, List *list2); //同上(递归方式)
List_node *find_mid_node(List *list) ; //得到链表中间节点的位置
List_node *find_revise_node(List *list, int conut) ; //得到链表的倒数第k个节点
List *reverse_list(List *list) ; //反转链表
List *list_dup(List *list) ; //链表的拷贝
void reverse_print_list(List *list) ; //逆序打印链表
Boolean is_list_intersect(List *list1, List *list2) ; //判断两个链表是否有交点
List_node *get_first_common_node(List *list1, List *list2) ; //找到两个链表的第一个交点
void delete_one_node(List *list, List_node *node) ; //在O(1)时间复杂度删除链表节点
Boolean has_circle1(List *list) ; //判断链表是否有环
Boolean has_circle2(List *list, List_node **intersect) ; //判断链表是否有环
List_node *find_circle_first_node1(List *list) ; //找到链表环的入口(简便)
List_node *find_circle_first_node2(List *list) ; //找到链表环的入口
1.链表初始化
List *init_list(void) //链表初始化
{
List *list = NULL;
//链表控制信息申请
list = (List *)Malloc(sizeof(List));
bzero(list, sizeof(List));
return list;
}
我们这里在动态内存分配中采用了Malloc函数,它是malloc函数的包裹函数,关于包裹函数本人在《unix网络编程》一书中第一次接触,因为库函数的相关操作会有失败的可能,如果对每次的操作都要进行直接的判断将会使我们的有效代码被淹没在各种判断中,所以建议大家把判断的过程采用包裹函数封装起来,关于函数调用产生的错误或者一异常问题都在包裹函数中单独处理,这样会使代码更加的简洁。
下面列出malloc包裹函数的具体实现:
static void *Malloc(size_t size)
{
void *result = NULL;
result = malloc(size);
if(result == NULL){
fprintf(stderr, "the memory is full!\n");
exit(1);
}
return result;
}
就想上述使用的那样,以后进行动态内存分配只需要调用Malloc即可。
2.链表的销毁
在处理完链表创建,紧接着需要处理的应该是链表的销毁。作为数据结构的处理一定要“善始善终”,这样才不会造成内存的泄露。
链表的销毁我们传入的是链表控制信息的指针的指针,关于细节的问题我们如下图所示:
图2
void destroy_list(List **list) //链表的销毁
{
if(list == NULL || *list == NULL){
return ;
}
//1.释放链表本身节点
while((*list)->count){
pop_front(*list);
}
//2.释放链表的控制信息
free(*list);
*list = NULL;
}
这里对于链表的释放在最后一个步骤对链表控制信息类型的指针进行了修改,因为虽然该指针还指向这个控制信息的地址,但是其控制信息已经被释放了,也就是说该指针指向了一个已经非法的地址,所以我们需要将该指针指向NULL。该地址是安全的。
关于push_front(头部插入)、push_back(尾部插入)、pop_front(头部删除)、pop_back(尾部删除)四个接口是作为一种底层容器的操作而存在的,这样对于线性容器来说,头和尾的四种增加和删除的操作都已经完备了,这在我们对于链表的封装,使其成为栈或者队列有着重要的意义。
四种接口的实现如下所示:
3.头部插入
图3:头部插入
Boolean push_front(List *list, int value) //头部插入
{
List_node *node = NULL;
if(list == NULL){
return FALSE;
}
node = buy_node();
node->data = value;
if(list->count == ZERO){ //链表没有元素
list->head =