目录

1、链表倒数第k个结点

2、反转链表

3、合并排序链表

4、复杂链表的复制

5、两个链表的第一个公共结点

6、链表中换的入口

7、删除链表中重复节点

8、单链表的中间节点

9、从尾到头打印链表

10、删除指定节点

11、 反转链表从N到M节点


链表是一种最基本的数据结构,在面试中常用来考察应聘者的基本功。指针操作是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;
	}
};

参考一

参考二