链表面试题
1. 从尾到头打印单链表
方法一:
设置一个指向链表尾部的指针end,每次遍历链表到该指针之前,打印结点,并将尾部指针end往前挪,直到end指针指向头结点时,就退出循环,完成打印。
void SListPrintTailToHead1(SListNode *phead){
assert(phead);
SListNode *end = NULL;//要打印结点的后一个结点
while(end != phead){
SListNode *cur = phead;
//找要打印的结点
while(cur->_next != end){
cur = cur->_next;
}
printf("%d\t",cur->_data);
//调整打印条件
end = cur;
}
}
方法二:
递归打印,将问题转化为子问题,有两种写法,但大体思路都是一样的。
void SListPrintTailToHead2(SListNode* pHead){
if(pHead->_next == NULL){
printf("%d\t",pHead->_data);
return;
}
SListPrintTailToHead2(pHead->_next);
//链表中除了pHead之外的所有结点都逆序打印了
printf("%d\t",pHead->_data);
}
void SListPrintTailToHead2_(SListNode* pHead){
if(pHead == NULL){
return;
}
SListPrintTailToHead2(pHead->_next);
//链表中除了pHead之外的所有结点都逆序打印了
printf("%d\t",pHead->_data);
}
2. 逆置/反转单链表
方法一:
创建一个空链表result用来保存逆置后的链表,然后从旧链表上每取出一个结点,头插到新链表上,直到旧链表为空,完成插入。不过要注意的一点是在取旧链表中的结点时,需要一个指针保存下一结点的位置,要不然改变了结点的next值,就无法找到结点的下一个结点。
SListNode* ReverseList(SListNode *phead){
SListNode *result = NULL;
SListNode *cur = phead;
SListNode *next =NULL;
while(cur != NULL){
next = cur->_next;
cur->_next = result;
result = cur;
cur = next;
}
return result;
}
方法二:
遍历链表,逆置每一个结点,从而达到逆置链表。cur遍历链表,next用于保存下一结点的位置,pre用于保存前一结点的位置,一开的时候将pre设为NULL。
SListNode* ReverseList2(SListNode *phead){
SListNode *cur = phead;
SListNode *pre = NULL;
SListNode *next = NULL;
while(cur != NULL){
next = cur->_next;
cur->_next = pre;
pre = cur;
if(next == NULL){
break;
}
cur = next;
}
return cur;
}
3. 删除一个无头单链表的非尾节点(不能遍历链表)
首先一般情况下要想删除一个结点无非是便利找到该结点的前一结点,然后删除该节点。但是这里要删除的是一个没有头结点的链表中的结点,这时我们就只能通过一点巧思路来解决这个问题:如下图,我们想要删除的是pos处的结点,只需要将pos的下一位置的结点的数值赋给我们pos,然后再删除pos的下一结点,就相当于删除了pos这个结点。这里有点像狸猫换太子,虽然删除的并不是我们指定的呢一个结点,但也达到了相同的结果。
void SListDelNonTailNode(SListNode* pos){
pos->_data = pos->_next->_data;
SListNode *del = pos->_next;
pos->_next = pos->_next->_next;
free(del);
}
4.在无头单链表的一个节点前插入一个节点(不能遍历链表)
这道题与上一道题很类似,都是替换的一个思想,就不赘述了。附上图供大家参考。
void SListInsertFrontNode(SListNode* pos, DataType x){
SListNode *newnode = BuySListNode(pos->_data);//创建一个结点
pos->_data = x;
newnode->_next = pos->_next;
pos->_next = newnode;
}
5.单链表实现约瑟夫环(JosephCircle)
约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。通常解决这类问题时我们把编号从0~n-1,最后结果+1即为原问题的解
这里我们通过单链表来实现约瑟夫环,首先从1开始报数,每数到3时重新从1开始报数,并且数到3的这个人出列,直到所有剩下最后一个人时,该玩家幸存。来简单模拟一下整个过程,假定有6个玩家参与游戏,这6个人围成一个圆,从1开始报数,第一次报到3的是3号玩家,该玩家出局;第二次报到3的是6号玩家;接下来是4、2、5玩家出局,最后就剩下1号玩家,该玩家胜出。在了解了大体思路后,该如何实现呢?其实不难,先得把单链表连成环;虽然这里是逢3就有玩家退出,但实际在链表中只用走2步,这是一个需要注意的地方,然后就是需要一个指针保存要删除结点的前一结点pre,循环结束的条件呢就是当链表中仅剩一个结点的时候。其他的都是链表中简单的操作,下面我们来实现一下。
SListNode* SListJosephCircle(SListNode* pHead, int k){
SListNode *cur = pHead;
SListNode *pre = NULL;
SListNode *tail = pHead;
while(tail->_next != NULL){
tail = tail->_next;
}
tail->_next = pHead;//将链表连成环
while(cur->_next != cur){
int n = k;
while(--n){
pre = cur;
cur = cur->_next;
}
SListNode *del = cur;//要删除的结点
pre->_next = cur->_next;
free(del);
cur = pre->_next;
}
return cur;
}
6.合并两个有序链表,合并后依然有序(升序)
合并两个有序链表,当两个链表都不为空的时候,比较两个链表数值的大小,当cur1->_data <= cur2->_data时我们把cur1插入到结果链表result中,否则把cur2插入到结果链表result中。当有一个链表为空的时候,将另一个链表直接插到结果链表中就完成了,当然我们插入到结果链表中采用的是尾插法,所以最好设置一个指针指向尾结点,便于插入。
SListNode* SListMerge(SListNode* list1, SListNode* list2){
SListNode *result = NULL;
SListNode *r = NULL;
SListNode *cur1 = list1;
SListNode *cur2 = list2;
while(cur1 != NULL && cur2 != NULL){
if(cur1->_data <= cur2->_data){
SListNode *next = cur1->_next;
if(result == NULL){
result = cur1;
r = result;
continue;
}
r->_next = cur1;
r = cur1;
cur1 = next;
}
else{
SListNode *next = cur2->_next;
if(result == NULL){
result = cur2;
r = result;
continue;
}
r->_next = cur2;
r = cur2;
cur2 = next;
}
}
if(cur1 == NULL){
r->_next = cur2;
}
else{
r->_next = cur1;
}
return result;
}
7.求两个已排序单链表中相同的数据
这道题本身来说不难,只是有一些字节需要注意,求两个有序单链表的交集,只需比cur1->_data以及cur2->_data的值,如果相等就直接输出,如果cur1->_data < cur2->_data时就让cur1指向下一个数,否则就让cur2指向下一个数。但仅仅这样做如果链表元素又重复,则会重复打印,这并不是我们预期的那样,这时我们就应当设置一个data值保存上一个打印的值,要是相等则指向下一个值。这样就很好的避免了重复的数据,但是需要注意cur1以及cur2的范围。
void Intersection(SListNode* l1, SListNode* l2){
SListNode *cur1 = l1;
SListNode *cur2 = l2;
int data;
while(cur1 != NULL && cur2 != NULL){
if(cur1->_data == cur2->_data){
data = cur1->_data;
printf("%d ",data);
while(cur1 != NULL && data == cur1->_data){
cur1 = cur1->_next;
}
while(cur2 != NULL && data == cur2->_data){
cur2 = cur2->_next;
}
}
else if(cur1->_data < cur2->_data){
cur1 = cur1->_next;
}
else{
cur2 = cur2->_next;
}
}
}
8.查找单链表的中间节点,要求只能遍历一次链表
快慢指针法:
首先设置一个快指针一个满指针,快指针每次走两步,慢指针每次走一步,当快指针走到链表的尾部的时候,慢指针指向的位置即为单链表的中间结点。
SListNode* SListFindMidNode(SListNode* list){
SListNode *fast = list;
SListNode *slow = list;
while(fast->_next != NULL){
fast = fast->_next;
if(fast == NULL){
break;
}
fast = fast->_next;
if(fast == NULL){
break;
}
slow = slow->_next;
}
return slow;
}
9.查找单链表的倒数第k个节点,要求只能遍历一次链表
前后指针法:
要找倒数第k个结点,我们可以让前面的指针先走k步,然后前后指针一起走,直到前指针走到链表的尾部时,后指针指向的就是倒数第k个结点。
SListNode* SListFindTailKNode(SListNode* list, size_t k){
SListNode *front = list;
SListNode *tail = list;
while(k--){
front = front->_next;
}
while(front != NULL){
front = front->_next;
tail = tail->_next;
}
return tail;
}
10.删除链表的倒数第K个结点
这道题可以在上道题的思路下继续做,要想删除倒数第k个结点,就要先找到倒数第k+1个结点,这里需要注意的是,要是链表只有k个结点的话就直接执行头删。找到倒数第k+1个结点后,便可直接删除倒数第k个结点。
void SListDelTailKNode(SListNode**list, size_t k){
SListNode *cur = *list;
int len = 0;
while(cur != NULL){
cur = cur->_next;
len++;
}
if(k == len){
SListPopFront(list);//执行头删
}
else{
SListNode *per = SListFindTailKNode(*list, k+1);//找到要删除结点的前一结点,也就是倒数第k+1个结点
SListNode *del = per->_next;
per->_next = per->_next->_next;
free(del);
}
}