链表相关笔试题
在笔试面试考数据结构时,由于时间有限,所出的题不会是红黑树、平衡二叉树等比较复杂的数据结构。链表结构简单,题目规模小但需要仔细考虑细节,因此称为笔试面试中的高频考点。因此,下面总结出链表相关题目,以供复习。
1.比较顺序表和链表的优缺点,说说他们分别在什么场景下使用?
2.从尾到头打印单链表(剑指offer第五题)
3.删除一个无头单链表的非尾节点
4.在无头单链表的一个非头结点前插入一个节点
5.单链表实现约瑟夫环
6.逆置/反转单链表
7.单链表排序(冒泡排序&快速排序)
8.合并两个有序链表合并后依然有序
9.查找链表的中间节点,要求只能遍历一次链表
10.查找单链表倒数第K个节点,要求只能遍历一次链表
11.判断单链表是否带环?若带环,求环的长度?求环的入口点?并计算每个算法的时间复杂度&空间复杂度。
12.判断两个链表是否相交,若相交,求交点(假设链表不带环)
13.判断两个链表是否相交,若相交,求交点(假设链表带环)
14.复杂链表的复制,一个链表的每个节点,有一个指向next指针指向下一个节点,还有一个random指针指向这个链表中的一个随机节点或者NULL,现在要求实现复制这个链表
15.求两个已排序链表中相同的数据。void UnionSet(ListNode* l1,ListNode* l2);
/////////////////////////////////////////////////////////////////////////////////////////////////////////(分割线)///////////////////////////////////////////////////
1.比较顺序表和链表的优缺点,说说他们分别在什么场景下使用?
首先我们从顺序表和链表的结构上来进行分析:
(1)对于顺序表,无论是动态的还是静态的,他们都是连续的存储空间,在读取上时间效率比较高,可通过地址之间的运算来访问,但是在插入和删除时会出现比较麻烦的负载操作。
(2)对于顺序表,因为是链式存储。因此在我们需要的时候我们才在堆上为他们开辟空间,链表对于插入删除比较简单,但是遍历的话需要多次跳转。
其次,我们从顺序表和链表的空间申请方式来看:
(1)对于顺序表,空间开辟是在顺序表已满的时候开辟,开辟次数较多的时候会出现较大的空间浪费
(2)对于链表,空间是针对单个节点的,不存在多余的空间浪费。并且在碎片内存池的机制下,可以有效的利用空间。
综上所述:顺序表一般用于查找遍历操作比较频繁的情况下使用,链表则针对于数据删除修改操作比较多的情况下使用。
2.从尾到头打印单链表
从尾到头打印单链表有两种解法,一种是利用栈把节点从头到尾push进去,利用栈先进后出的特点,从尾到头打印单链表节点,一种是利用递归,在输出现有节点之前输出下一个节点,循环直至最后一个节点,然后再将节点从尾到头依次打印。
code1:利用栈
void PrintTailToHead(ListNode* head)
{
stack<int> st;
ListNode* p = head;
while (p != NULL)
{
st.push(p->_data);
p = p->_next;
}
while (!st.empty())
{
printf("%d->", st.top());
st.pop();
}
}
code2:利用递归
void PrintTailToHead(ListNode* head)
{
if (head != NULL)
{
while (head->_next != NULL)
{
PrintTailToHead(head->_next);
}
}
printf("%d->", head->data);
}
3.删除一个无头单链表的非尾节点
由于链表无头,所以用常规方法删除节点是不可能的。所以我们可以换种思路,将要删除的节点后面的节点的值赋给要删的节点,然后再把要删除节点的后面的节点删除,等于通过转换,为被删除节点创造了一个头结点。代码如下:
void DeleteNotTailNode(ListNode* p)
{
ListNode* s = p->_next;
assert(s);
p->_data = s->_data;
p->_next = s->_next;
free(p);
}
4.在无头单链表的一个非头结点前插入一个节点
这个题目跟上一个题目很像。在这个非头结点后面插入一个节点,把这个非头节点的值赋给新插入的节点,然后再把要插入的值赋给这个非头节点即可。
void InsertNotHeadNode(ListNode* p, int data)
{
ListNode* s = (ListNode)malloc(sizeof(&ListNode));
assert(s);
s->_next =p->_next;
p->_next = s;
s->_data = p->_data;
p->_data = data;
}
5.单链表实现约瑟夫环(剑指offer第45题)
6.逆置/反转单链表(剑指offer第16题)
7.单链表排序(冒泡排序&快速排序)
8.合并两个有序链表合并后依然有序(剑指offer第17题)
这个题比较简单,分别用指针指向两个链表,比较两个链表指针所指向节点的值,然后将节点取下来重新组成一个链表即可,代码如下:
ListNode Merge(ListNode* head1, ListNode* head2)
{
if (head1 == NULL)
return head2;
if (head2 == NULL)
return head1;
ListNode* newhead = NULL;
if (head1->_data < head2->_data)
{
newhead = head1;
newhead->_next=Merge(head1->_next, head2);
}
if (head1->_data>head2->data)
{
newhead = head2;
newhead->_next = Merge(head1, head2->_next);
}
}
9.查找链表的中间节点,要求只能遍历一次链表
查找链表的中间节点,但只能遍历一次链表,所以我们会想到用快慢指针来解决这个问题。定义一个快指针,每次走两步,载定义一个慢指针,每次走一步。等到快指针走到链表尾,慢指针所指向的节点就是链表的中间节点。代码如下:
ListNode* FindMidNode(ListNode* head)
{
ListNode* fast;
ListNode* slow;
fast = head;
slow = head;
while (fast&&fast->_next)
{
slow = slow->_next;
fast = fase->_next->_next;
}
retrun slow;
}
10.查找单链表倒数第K个节点,要求只能遍历一次链表(剑指offer第15题)
其实这个题跟上面的题很像,稍微转化一下就能想出思路。我们可以定义两个指针,一个指针先走K步,然后两个指针同时移动,等到先走的指针走到链表尾部,后走的指针所指向的节点就是倒数第K个节点。要注意考虑链表的各种情况。代码如下:
ListNode* FindKthNode(ListNode* head,int k)
{
if (head == NULL || k == 0)
return NULL;
ListNode* fast;
ListNode* slow;
fast = head;
slow = head;
for (int i = 0; i < k - 1; ++i) //要注意链表长度比K短的情况
{
if (fast->_next != NULL)
fast = fast->_next;
else retrun NULL;
}
while (fast->_next != NULL)
{
fast = fast->_next;
slow = slow->_next;
}
return slow;
}
11.判断单链表是否带环?若带环,求环的长度?求环的入口点?并计算每个算法的时间复杂度&空间复杂度。(剑指offer第56题)
12.判断两个链表是否相交,若相交,求交点(假设链表不带环)
13.判断两个链表是否相交,若相交,求交点(假设链表带环)
14.复杂链表的复制,一个链表的每个节点,有一个指向next指针指向下一个节点,还有一个random指针指向这个链表中的一个随机节点或者NULL,现在要求实现复制这个链表(剑指offer第26题)
15.求两个已排序链表中相同的数据。void UnionSet(ListNode* l1,ListNode* l2);
16.在已排序的链表中删除链表中重复的结点(剑指offer第57题)