LeetCode总结:双指针在链表中的应用

  1. 删除链表的倒数第n个节点——LeetCode 19
      思路:使用两个指针,第一个指针first先走n步,若此时first为空,则待删除的节点是第一个节点;若first不为空,则同时将first和second指针向前移动,并且first指针超前second指针n个节点,当first为空时,second指向的节点即为倒数第n个节点。需要注意的是,由于要删除倒数第n个节点,故需要知道倒数第n+1个节点的位置,所以应对first->next是否为空进行判断。
ListNode* removeNthFromEnd(ListNode* head, int n) {
	if (!head) return NULL;

	ListNode *first = head;
	ListNode *second = first;

	/*first指针先走n步,此时若first为空,则要删除的节点是首节点*/
	for (int i = 0; i < n; i++) {
		first = first->next;
	}
	if (!first) {
		return head->next;
	}

	/*两个指针同时走,当first->next为空时,second指向的下一个节点即为倒数第n个节点*/
	while (first->next) {
		first = first->next;
		second = second->next;
	}
	second->next = second->next->next;

	return head;
}
  1. 查找链表的中间节点——LeetCode 876
      思路:同样使用两个节点,两个指针同时移动,第一个指针每次移动两步,第二个节点每次移动一步,则第一个指针到达链表的最后一个节点时,第二个指针到达中间节点。
 ListNode* middleNode(ListNode* head) {
     if(!head) return NULL;
     
     ListNode *first = head, *second = head;
     while(first->next){
         first = first->next;
         second = second->next;  // 让second提前走,可保证节点数为偶数时,second指向中间靠后节点
         if(first->next) first = first->next;
     } 

     return second;
 }
  1. 判断链表是否有环——LeetCode 141
      思路:使用两个指针,first指针每次向前移动两个节点。slow指针每次移动一个节点。若链表中有环,则两个指针一定会相遇。
      完整的测试代码如下:
#include

using namespace std;

struct ListNode {
	int val;
	ListNode *next;
	ListNode(int x) : val(x), next(NULL) {}
};

ListNode* MakeList();
void ListAppend(ListNode*, int);
ListNode* ListMakeCycle(ListNode*, int);
void Print(ListNode* head, ListNode* start);
bool hasCycle(ListNode *head);

int main() {
	ListNode *head = MakeList();
	int value;

	cout << "Please input the value of the node : " << endl;
	while (cin >> value) {
		ListAppend(head, value);
		if (cin.get() == '\n') {
			break;
		}
	}

	cout << "Please choose the start node of the cycle : " << endl;
	int n;
	cin >> n;
	ListNode *start = ListMakeCycle(head, n);
	cout << "The linked list with cycle : " << endl;
	Print(head, start);

	bool IsCycle = hasCycle(head->next);
	cout << "\nIs cycle ? " << IsCycle << endl;

	return 0;
}

/* 生成链表的表头 */
ListNode* MakeList() {
	ListNode *head = new ListNode(-1);
	return head;
}

/* 在链表尾部插入节点 */
void ListAppend(ListNode* head, int value) {
	if (!head) return;

	ListNode *tmp = head;
	while (tmp->next) {
		tmp = tmp->next;
	}
	ListNode *node = new ListNode(value);
	tmp->next = node;
}

/* 将链表的最后一个节点接到第n个节点,生成环,并返回环的起点 */
ListNode* ListMakeCycle(ListNode* head, int n) {
	if (!head || !n) return NULL;

	ListNode *end = head->next;
	ListNode *start = end;

	int count = 1;
	while (end->next) {
		if (count != n) {
			start = start->next;
			count++;
		}
		end = end->next;
	}
	end->next = start;
	//cout << "\nstart node : " << start->val << endl;
	
	return start;
}

/* 打印链表,并将环打印两遍 */
void Print(ListNode *head, ListNode *start) {
	if (!head) return;

	ListNode *tmp = head->next;
	int count = 0;
	while (true) {
		if (tmp == start) {
			count++;
			if (count == 3) {
				break;
			}
		}
		cout << tmp->val << ' ';
		tmp = tmp->next;
	}
}

/* 判断链表是否有环 */
bool hasCycle(ListNode *head) {
	if (!head || !head->next) return false;

	ListNode *slow = head;
	ListNode *fast = head->next;

	while (fast != NULL && fast->next != NULL) {
		fast = fast->next->next;    // 快指针每次走两步
		slow = slow->next;	        // 慢指针每次走一步
		if (fast == slow) {
			return true;
		}
	}
	return false;
}

  输出结果:

LeetCode总结:双指针在链表中的应用_第1张图片

  1. 寻找一个带环链表的环的起点——LeetCode 142
      思路:分为两步进行,先判断是否有环;若有环,再寻找环的起点。判断是否有环和上一个例题相同,设置两个快慢指针,快指针每次移动两个节点,慢指针每次移动一个节点,若两个指针相遇,则链表有环。设两个指针相遇的节点为c,如下图所示:

示意图
图片来源:(Leetcode 142)Linked List Cycle (II) (快慢指针详解)

  下面寻找环的起点。快指针继续指向两个指针第一次相遇的节点c,将满指针指向链表的首个节点h,然后两个指针每次同步向前移动一个节点,当两个指针再次相遇时,相遇的节点即为环的起点。
  

ListNode *detectCycle(ListNode *head) {
	if (!head || !head->next) return NULL;

	ListNode *fast = head;
	ListNode *slow = fast;

	while (fast && fast->next) {
		fast = fast->next->next;
		slow = slow->next;
		if (fast == slow) {		// 快慢指针相遇,则链表有环
			break;
		}
	}
	if (!fast || !fast->next) {	// 链表无环(节点数为奇数时,fast为空;为偶数时,fast->next为空)
		return NULL;
	}

	slow = head;	// 快指针从两个指针相遇的地方出发,慢指针从链表的首个节点出发,同步移动
	while (fast != slow) {		// 两个指针再次相遇的节点即为环的起点
		fast = fast->next;
		slow = slow->next;
	}
	return fast;
}

  结果如下:(输入输出部分同上以例题)
LeetCode总结:双指针在链表中的应用_第2张图片

  1. 单链表翻转——LeetCode 206
      思路1:开辟新的内存,生成一个新的链表,采用尾插法,从后向前地将原链表的节点依次插入新的链表。这种方法比较简单,但浪费空间,在此不赘述。
      思路2:采用两个指针进行原地操作。原理入下:
    LeetCode总结:双指针在链表中的应用_第3张图片
    (图片来自中国大学MOOC浙江大学《数据结构》)

  上图适用于翻转链表中的K个节点。如上图所示,指针new指向链表中已翻转部分的首个节点,指针old指向链表中未翻转链表的首个节点。指针向前遍历的过程中,依次将old指向new,逐个翻转节点,直至遍历结束,即old为空。(上图中的链表是带头节点的,LeetCode中的链表均不带头节点,编写代码时需注意。)

ListNode* reverseList(ListNode* head) {
	ListNode *rev = NULL;			// 指向链表中已翻转部分的头部
	ListNode *unrev = head;			// 指向链表中未翻转部分的头部

	while (unrev) {
		ListNode *tmp = unrev->next;	// 保存未翻转部分头节点的位置
		unrev->next = rev;				// 节点翻转
		rev = unrev;					// 更新rev
		unrev = tmp;					// 更新unrev
	}

	return rev;
}

  在本题中需要注意的是rev指针的初始位置,rev初始位置应指向NULL而不是链表的首个节点。若rev初始位置为链表的首个节点,由于其不为空,则整个链表翻转完成后,以上图为例,最后一个节点1的下个节点不是空节点,而是翻转之前的链表中1的下一个节点2,即翻转后链表为6,5,4,3,2,1,2,1,2,1… 。(自己就踩了这个坑。。。。。。)

  1. 翻转单链表中的部分节点——LeetCode92
      思路与上一题完全相同,只需确定翻转的起点和终点即可。
ListNode* reverseBetween(ListNode* head, int m, int n) {
	ListNode *rev = new ListNode(-1);	// 指向链表中已翻转部分的头部
	rev->next = head;
	
	int num = 0;
	while (num < m - 1) {
		if (rev->next) {
			rev = rev->next;
			num++;
		}
	}	
	ListNode *flag = rev;				// 此时rev指向待翻转节点的前一个节点
	ListNode *unrev = rev->next;		// 指向链表中未翻转部分的头部

	int count = 0;
	int k = n - m + 1;
	while (count < k) {
		ListNode *tmp = unrev->next;	// 保存未翻转部分头节点的位置
		unrev->next = rev;				// 节点翻转
		rev = unrev;					// 更新rev
		unrev = tmp;					// 更新unrev
		count++;
	}

	flag->next->next = unrev;
	flag->next = rev;

	if (m == 1) {
		return flag->next;
	}
	else {
		return head;
	}
}
  1. 交换每一对节点的位置——LeetCode24
      原理如下:
    LeetCode总结:双指针在链表中的应用_第4张图片
    代码:
ListNode* swapPairs(ListNode* head) {
	ListNode *ehead = new ListNode(-1);
	ehead->next = head;
	ListNode *pre = ehead, *cur = head;
	while (cur && cur->next) {
		pre->next = cur->next;
		cur->next = pre->next->next;
		pre->next->next = cur;

		pre = cur;
		cur = cur->next;
	}
	return ehead->next;
}
  1. 翻转链表中的所有包含K个节点的一组节点——LeetCode25
      这一题是上一题的拓展。上一题是每次翻转两个节点,这一题是每次翻转K个节点。所以这一题的思路与上一题类似,只是每一组节点需要做连续翻转。除此之外,在进行翻转之前,需确定是否满足翻转的条件,即待翻转链表的长度要大于等于K。
ListNode* reverseKGroup(ListNode* head, int k) {
	ListNode *dummy = new ListNode(-1);
	dummy->next = head;
	ListNode *pre = dummy, *cur = pre->next;
	int len = 0;
	while (cur) {
		len++;
		cur = cur->next;
	}
	cur = pre->next;

	while (len >= k) {
		for (int i = 1; i < k; i++) {
			ListNode *t = pre->next;		// 记录当前要翻转节点翻转后要指向的节点
			pre->next = cur->next;			// 记录下一次要翻转的节点翻转后要指向的节点(即当前要翻转的节点)
			cur->next = pre->next->next;	// 记录下一次要翻转的节点
			pre->next->next = t;	// 节点翻转:每次翻转的是cur指向的下一个节点
		}
		pre = cur;
		cur = pre->next;
		len -= k;
	}
	return dummy->next;
}
  1. 链表重排——LeetCode143
      先将链表的后半部分(不包括中间节点)翻转,再 将这后半链表的节点依次插入到前半链表的相应位置。
void reorderList(ListNode* head) {
   if (!head || !head->next) return;

   ListNode *fast = head, *slow = head;
   while (fast->next && fast->next->next) {
   	slow = slow->next;	
   	fast = fast->next->next;
   }	// 此时first指向链表中点

   fast = slow->next;	// 从fast节点开始翻转链表
   ListNode *rev = NULL;
   while (fast) {
   	ListNode *tmp = fast->next;
   	fast->next = rev;
   	rev = fast;
   	fast = tmp;
   }
   slow->next = rev;

   fast = head;		// 两个指针分别从头和中间向后遍历
   while (slow->next && fast != slow) {
   	ListNode *t1 = fast->next;
   	ListNode *t2 = slow->next->next;
   	fast->next = slow->next;
   	fast->next->next = t1;
   	slow->next = t2;
   	fast = t1;
   }
}
  1. 判断链表是否是回文链表——LeetCode234
      要求O(n)时间和O(1)空间,则先将链表的前半部分或后半部分翻转,再依次比较前后半个链表的节点是否相同。需要注意链表的节点数量的奇偶性,为奇数时,寻找中间节点的slow指针指向节点中点,该节点不翻转;为偶数时,slow指针指向两个中间节点的前一个,该节点需要翻转。
bool isPalindrome(ListNode* head) {
	if (!head || !head->next) return true;

	ListNode *fast = head, *slow = head;
	while (fast->next && fast->next->next) {
		fast = fast->next->next;
		slow = slow->next;
	}

	bool isEven = false;
	if (fast->next) {		// fast为倒数第二个节点,则节点数量为偶数,要翻转中间节点
		slow = slow->next;
		isEven = true;
	}

	fast = head;
	ListNode *rev = NULL;
	while (fast != slow) {	// 翻转前半个链表		
		ListNode *tmp = fast->next;
		fast->next = rev;
		rev = fast;
		fast = tmp;
	}
	head->next = fast;		// 此时rev为表头

	//PrintList(rev);

	if (!isEven) {	// 节点数量为奇数时,要从中点的后一个节点比较
		slow = slow->next;
	}	
	while (slow) {	// 比较翻转后的链表的前后半部分是否相同 
		if (rev->val != slow->val) {
			return false;
		}
		else {
			rev = rev->next;
			slow = slow->next;
		}
	}
	return true;
}

你可能感兴趣的:(LeetCode,算法)