目录
链表是一种最基本的数据结构,在面试中常用来考察应聘者的基本功。指针操作是C/C++中的难点,也是易错点,而链表的操作就是一系列指针操作。所以,链表相关题目在面试中占据着很重要的地位。本文整理了剑指offer中的链表题目以及自己面试中遇到的链表题目进行了总结,希望能对找工作的同学有所帮助。
//定义链表结构体
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};
1、链表倒数第k个结点
题目描述:输入一个链表,输出该链表中倒数第k个结点。
思路:两个指针,先让第一个指针和第二个指针都指向头结点,然后再让第一个指正走(k-1)步,到达第k个节点。然后两个指针同时往后移动,当第一个结点到达末尾的时候,第二个结点所在位置就是倒数第k个节点了
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
if (pListHead == NULL || k == 0) return NULL;
ListNode* pFirst = pListHead;
ListNode* pSecond = NULL;
for (unsigned i = 0; i < k - 1; i++){ //第一个指针先向前走k-1步
if (pFirst->next != NULL)
pFirst = pFirst->next;
else return NULL;
}
pSecond = pListHead;
while (pFirst->next != NULL){ //两个指针同步向后走,一直到第一个指针走到链表的末尾
pFirst = pFirst->next;
pSecond = pSecond->next;
}
return pSecond;
}
};
2、反转链表
题目描述:输入一个链表,反转链表后,输出新链表的表头。
思路:从头到尾遍历原链表,每遍历一个结点,将其摘下放在新链表的最前端。注意链表为空和只有一个结点的情况。时间复杂度为O(n)。
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
//考虑到代码的鲁棒性,先来一步判定链表是否为空;
if (pHead == NULL) return NULL;
ListNode* pReverseHead = NULL; //反转后的链表头结点
ListNode* pNode = pHead; //反转时的当前节点
ListNode* pPrev = NULL; //当前节点的前一个节点
while (pNode != NULL){
ListNode* pNext = pNode->next; //当前节点的后一个节点
if (pNext == NULL)
pReverseHead = pNode;
//以下三行是关键
pNode->next = pPrev; //指针反转
pPrev = pNode;
pNode = pNext;
}
return pReverseHead;
}
};
当然也可以使用递归。
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
//如果链表为空或者链表中只有一个元素
if(pHead==NULL||pHead->next==NULL) return pHead;
//先反转后面的链表,走到链表的末端结点
ListNode* pReverseNode=ReverseList(pHead->next);
//再将当前节点设置为后面节点的后续节点
pHead->next->next=pHead;
pHead->next=NULL;
return pReverseNode;
}
};
3、合并排序链表
题目描述:输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
思路:1)链表1的头结点的值小于链表2的头结点的值,因此链表1的头结点是合并后链表的头结点;
2)在剩余的结点中,链表2的头结点的值小于链表1的头结点的值,因此链表2的头结点是剩
3)余结点的头结点,把这个结点和之前已经合并好的链表的尾结点链接起来。
class Solution {
public:
ListNode* Merge(ListNode* list1, ListNode* list2){
if (list1 == NULL) return list2;
if (list2 == NULL) return list1;
ListNode *mergeHead = NULL,*current = NULL;
while (list1 != NULL && list2 != NULL){
if (list1->val <= list2->val){
if (mergeHead == NULL) mergeHead = current = list1;
else current->next = list1,current = current->next;
list1 = list1->next;
}
else {
if (mergeHead == NULL) mergeHead = current = list2;
else current->next = list2,current = current->next;
list2 = list2->next;
}
}
if (list1 == NULL) current->next = list2;
else current->next = list1;
return mergeHead;
}
};
递归实现:
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
ListNode* node=NULL;
if(pHead1==NULL){return node=pHead2;}
if(pHead2==NULL){return node=pHead1;}
if(pHead1->val>pHead2->val){
node=pHead2;
node->next=Merge(pHead1,pHead2->next);
}else{
node=pHead1;
node->next=Merge(pHead1->next,pHead2);
}
return node;
}
};
4、复杂链表的复制
题目描述:输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
思路:
class Solution {
public:
RandomListNode* Clone(RandomListNode* pHead)
{
if(!pHead) return NULL;
RandomListNode *currNode = pHead;
while(currNode){
RandomListNode *node = new RandomListNode(currNode->label);
node->next = currNode->next;
currNode->next = node;
currNode = node->next;
}
currNode = pHead;
while(currNode){
RandomListNode *node = currNode->next;
if(currNode->random){
node->random = currNode->random->next;
}
currNode = node->next;
}
//拆分
RandomListNode *pCloneHead = pHead->next;
RandomListNode *tmp;
currNode = pHead;
while(currNode->next){
tmp = currNode->next;
currNode->next =tmp->next;
currNode = tmp;
}
return pCloneHead;
}
RandomListNode* pHead;
};
5、两个链表的第一个公共结点
题目描述:输入两个链表,找出它们的第一个公共结点。
思路:假定 List1长度: a+n List2 长度:b+n, 且 a<b,那么 p1 会先到链表尾部, 这时p2 走到 a+n位置,将p1换成List2头部,接着p2 再走b+n-(n+a) =b-a 步到链表尾部,这时p1也走到List2的b-a位置,还差a步就到可能的第一个公共节点。将p2 换成 List1头部,p2走a步也到可能的第一个公共节点。如果恰好p1==p2,那么p1就是第一个公共节点。或者p1和p2一起走n步到达列表尾部,二者没有公共节点,退出循环。 同理a>=b。时间复杂度O(n+a+b)!
class Solution {
public:
ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2) {
ListNode *p1 = pHead1;
ListNode *p2 = pHead2;
while (p1!=p2)
{
if(p1!=NULL)
p1 = p1->next;
else p1 = pHead2;
if (p2 != NULL)
p2 = p2->next;
else p2 = pHead1;
}
return p1;
}
};
6、链表中换的入口
题目描述:给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
思路:
假设x为环前面的路程(黑色路程),a为环入口到相遇点的路程(蓝色路程,假设顺时针走), c为环的长度(蓝色+橙色路程)
当快慢指针相遇的时候:此时慢指针走的路程为Sslow = x + m * c + a,快指针走的路程为Sfast = x + n * c + a,2 Sslow = Sfast,
2 * ( x + m*c + a ) = (x + n *c + a);
从而可以推导出:x = (n - 2 * m )*c - a = (n - 2 *m -1 )*c + c - a
即环前面的路程 = 数个环的长度(为可能为0) + c - a,什么是c - a?这是相遇点后,环后面部分的路程。(橙色路程)
所以,我们可以让一个指针从起点A开始走,让一个指针从相遇点B开始继续往后走,2个指针速度一样,那么,当从原点的指针走到环入口点的时候(此时刚好走了x),从相遇点开始走的那个指针也一定刚好到达环入口点。所以2者会相遇,且恰好相遇在环的入口点。最后,判断是否有环,时间复杂度:O(n),空间复杂度:O(1)。
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
if (!pHead->next)
return NULL;
ListNode* previous = pHead;
ListNode* front = pHead ->next;
while (front)
{
previous->next = NULL;
previous = front;
front = front->next;
}
return previous;
}
};
7、删除链表中重复节点
题目描述:在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5。
思路:
1.)加一个头结点
2.)两个临时指针p,q
3.)找前后不相等的节点
class Solution {
public:
ListNode* deleteDuplication(ListNode* pHead){
if (pHead == NULL || pHead->next == NULL)
return pHead;
/*---------先为链表创建一个头结点---------*/
int firstNumber = pHead->val;
//假设我的头结点数值为-1
int myFirst = -1;
//万一链表的头结点也为-1,那么我就改成-2
if (myFirst == firstNumber) myFirst = -2;
ListNode *head = new ListNode(myFirst);
head->next = NULL;
head->next = pHead;
ListNode *p = head;
ListNode *q = head->next;
while (q){
while (q->next && (q->next->val == q->val))
q = q->next;
if (p->next != q){
q = q->next;
p->next = q;
}
else{
p = q;
q = q->next;
}
}
//返回的时候,注意去掉头结点(自己创建的辅助节点)
return head->next;
}
};
8、单链表的中间节点
题目描述:输入一个链表,找到链表中间节点。
思路:设置两个指针,只不过这里是,两个指针同时向前走,前面的指针每次走两步,后面的指针每次走一步,前面的指针走到最后一个结点时,后面的指针所指结点就是中间结点,即第(n/2+1)个结点。注意链表为空,链表结点个数为1和2的情况。时间复杂度O(n)。
class solution {
public:
// 获取单链表中间结点,若链表长度为n(n>0),则返回第n/2+1个结点
ListNode * GetMiddleNode(ListNode * pHead){
if (pHead == NULL || pHead->next == NULL) // 链表为空或只有一个结点,返回头指针
return pHead;
ListNode * pAhead = pHead;
ListNode * pBehind = pHead;
while (pAhead->next) {// 前面指针每次走两步,直到指向最后一个结点,后面指针每次走一步
pAhead = pAhead->next;
pBehind = pBehind->next;
if (pAhead->next)
pAhead = pAhead->next;
}
return pBehind; // 后面的指针所指结点即为中间结点
}
};
9、从尾到头打印链表
思路:利用栈先进后出,后进先出的特性。
class solution{
public:
//使用栈
void PrintList(ListNode * pHead){
stack<ListNode*> s;
ListNode * pNode = pHead;
while (pNode){
s.push(pNode);
pNode = pNode->next;
}
while (!s.empty()){
pNode = s.top();
printf("%d\t", pNode->val);
s.pop();
}
}
//使用递归
void PrintList2(ListNode * pHead){
if (pHead == NULL) return;
else{
PrintList2(pHead->next);
printf("%d\t", pHead->val);
}
}
};
10、删除指定节点
题目描述:给出一单链表头指针pHead和一节点指针pToBeDeleted,O(1)时间复杂度删除节点pToBeDeleted。
思路:让该节点的前一个节点指向该节点的下一个节点,这种情况需要遍历找到该节点的前一个节点,时间复杂度为O(n)。对于链表,链表中的每个节点结构都是一样的,所以我们可以把该节点的下一个节点的数据复制到该节点,然后删除下一个节点即可。要注意最后一个节点的情况,这个时候只能用常见的方法来操作,先找到前一个节点,但总体的平均时间复杂度还是O(1)。
class solution {
public:
void Delete(ListNode * pHead, ListNode * pToBeDeleted)
{
if (pToBeDeleted == NULL) return;
if (pToBeDeleted->next != NULL){// 将下一节点的数据复制到本节点,然后删除下一个节点
pToBeDeleted->val = pToBeDeleted->next->val;
ListNode * temp = pToBeDeleted->next;
pToBeDeleted->next = pToBeDeleted->next->next;
delete temp;
}
else { // 要删除的是最后一个节点
if (pHead == pToBeDeleted) { // 链表中只有一个节点的情况
pHead = NULL;
delete pToBeDeleted;
}
else{
ListNode * pNode = pHead;
while (pNode->next != pToBeDeleted) // 找到倒数第二个节点
pNode = pNode->next;
pNode->next = NULL;
delete pToBeDeleted;
}
}
}
};
11、 反转链表从N到M节点
题目描述:输入一个链表,翻转链表从N到M个节点。
输入描述:1,2,3,4,5,6 3,5
输出描述:1,2,5,4,3,6
typedef struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
}node;
class Solution {
public:
node *reverseBetween(node *head, int m, int n) {
if (head == NULL) return NULL;
node *dummy = new node(0);
dummy->next = head;
node *pre, *start = head;
for (int i = 1; i < m; i++) {
pre = start;
start = start->next;
}
for (int i = m; i < n; i++) {
node *temp = start->next;
start->next = temp->next;
temp->next = pre->next;
pre->next = temp;
}
return dummy->next;
}
};