目录
1、单链表翻转
typedef struct node{
int value;
struct node* next;
}node;
void reverse(node* head)
{
if ( (head == NULL) || (head->next == NULL) )
return;// 边界检测
node* pPrev = NULL;
node* pCur = head; // 保存链表头节点
while (pCur != NULL)
{
node* pNext = pCur->next; // 暂时将下一个节点保存下来,
pCur->next = pPrev; // 将当前节点的 Next 指向前一个节点
pPrev = pCur; // 此时 当前节点 设置成上一节点
pCur = pNext; // 将保存的 pNext 设置为当前节点
}
return head = pPrev; //while 循环跳出条件是pCur==null,以返回了pPrev作为新的头指针
}
//while循环版
void reverse(node*& head)
{
if ( (head == NULL) || (head->next == NULL) ) return;// 边界检测
node* pPrev = head;// 保存链表头节点
node* pCur = head->next;// 获取当前节点
node* pNext = NULL; // 下一个结点
while (pCur != NULL)
{
pNext = pCur->next;// 将下一个节点保存下来
pCur->next = pPrev;// 将当前节点的下一节点置为前节点
pPrev = pCur;// 将当前节点保存为前一节点
pCur = pNext;// 将当前节点置为下一节点
}
// 重新设置头结点指向
head->next = NULL;
head = pPrev;
}
//递归版
LIST *reverseList2(LIST *phead)
{
LIST *current = phead;
//当检测到链表为空,或者已经检测到尾节点时,满足终止条件,停止向下递归
if(NULL == current || NULL == current->next){
return current;
}
//执行递归操作,寻找尾节点
LIST *newhead = reverseList2(current->next);
//反转操作,目的是完成当前节点的下一个节点,完成指向当前节点的操作
current->next->next = current;
//该语句很重要!
current->next = NULL;
printf("Reverse2 done!\n");
return newhead;
}
递归方法
先反转后面的链表,从最后面的两个结点开始反转,依次向前,将后一个链表结点指向前一个结点,注意每次反转后要将原链表中前一个结点的指针域置空,表示将原链表中前一个结点指向后一个结点的指向关系断开。
单链表翻转:https://www.jianshu.com/p/84117123f709
https://www.cnblogs.com/csbdong/p/5674990.html
LeetCode :力扣
2、只给定链表中需要删除的节点node,删除这个节点。
注意:前提不知道头结点,没法遍历
LeetCode上原题:力扣
分析:如果有头结点,对时间复杂度没有要求,直接遍历节点就可以,时间复杂度为O(n)
要求时间复杂度为O(1),此种情况下只有一种情况不能达到O(1)。
链表删除,可以考虑不单单删除节点,而是修改需要节点值为下一个节点的值,同时把需要删除节点的next指向下下个节点。
下面就是代码,代码就是这么简单
//node 要删除的结点,不知道头结点
public void deleteNode(ListNode node) {
node.val = node.next.val; //将node结点下一个结点的值赋值给node.val
node.next = node.next.next; //将node结点的下一个结点的next指向地址赋给 node.next
}
方法:node.value --- 需要删除的节点值
node.next ---需要删除节点的下一个节点
del.next.value --下一个节点的值
del.next.next -- 下一个结点指向的下一个结点
特殊情况是:del节点本身就是最后一个节点,这样的话 del.next = NULL,这种方法没法删除
3、判断链表是否有环,如果有,找到环的入口位置
判断一个链表是否有环,空间复杂度是O(1)
如果不考虑空间复杂度,可以使用一个map记录走过的节点,当遇到第一个在map中存在的节点时,就说明回到了出发点,即链表有环,同时也找到了环的入口。
不适用额外内存空间的技巧是使用快慢指针,即采用两个指针walker和runner,walker每次移动一步而runner每次移动两步。当walker和runner第一次相遇时,证明链表有环
以图片为例,假设环的长度为 R
,当慢指针walker
走到环入口时快指针runner
的位置如图,且二者之间的距离为S
。在慢指针进入环后的t
时间内,快指针从距离环入口 S
处走了2t
个节点,相当于从环入口走了S+2t
个节点。而此时慢指针从环入口走了t个节点。
假设快慢指针一定可以相遇,那么有 S+2t−t=nR
,即 S+t=nR
,如果对于任意的 S,R,n
,总可以找到一个t
满足上式,那么就可以说明快慢指针一定可以相遇,满足假设(显然可以找到)
而实际上,由于 S<R,所以在慢指针走过一圈之前就可以相遇
所以如果链表中有环,那么当慢指针进入到环时,在未来的某一时刻,快慢指针一定可以相遇,通过这个也就可以判断链表是否有环。
代码如下
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* };
*/
//使用快慢指针
public boolean hasCycle(ListNode head) {
//边界条件判断
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
// 将链表的指针放入set中,每次加入时都去set中查询
public boolean hasCycle(ListNode head) {
Set<ListNode> nodesSeen = new HashSet<>();
while (head != null) {
if (nodesSeen.contains(head)) {
return true;
} else {
nodesSeen.add(head);
}
head = head.next;
}
return false;
}
参考:LeetCode:力扣
3.1、如果链表有环,寻找环入口位置
上一个问题得到结论:慢指针走过一圈之前就可以相遇
以图片为例,假设环入口距离链表头的长度为L,快慢指针相遇的位置为 cross
,且该位置距离环入口的长度为S。考虑快慢指针移动的距离,慢指针走了 L+S
,快指针走了L+S+nR
(这是假设相遇之前快指针已经绕环n圈)。由于快指针的速度是慢指针的两倍,相同时间下快指针走过的路程就是慢指针的两倍,所以有2(L+S)=L+S+nR
,化简得L+S=nR
当n=1
时,即快指针在相遇之前多走了一圈,即L+S=R
,也就是L=R−S
,观察图片,L表示从链表头到环入口的距离,而R−
S表示从cross
继续移动到环入口的距离,既然二者是相等的,那么如果采用两个指针,一个从表头出发,一个从cross
出发,那么它们将同时到达环入口。即二者相等时便是环入口节点
当n>1
时,上式为L=nR−S
,L仍然表示从链表头到达环入口的距离,而nR−S
可以看成从cross
出发移动nR
步后再倒退S步,而明显从cross
移动nR步后回到cross
位置,倒退S步后是环入口,所以也是同时到达环入口。即二者相等时便是环入口节点
所以寻找环入口的方法就是采用两个指针,一个从表头出发,一个从相遇点出发,一次都只移动一步,当二者相等时便是环入口的位置
ListNode *findEnterNode(ListNode *head) {
ListNode * slow = head;
ListNode * fast = head;
while(fast != null && fast->next != null)
{
slow = slow->next;
fast = fast->next->next;
//两指针相遇则有环
if(slow == fast)
break;
}
if(!fast || !fast->next)
return nullptr;
ListNode * headWalker = head; // 从head开始走
ListNode * crossWalker = slow; // 从相遇点开始走
//两指针相遇的点即是入口节点, 前面已经证明了链表有环,并且不为空
while(headWalker != crossWalker)
{
headWalker = headWalker->next;
crossWalker = crossWalker->next;
}
return headWalker;
}
https://www.jianshu.com/p/7608f44e1baf
【图解LeetCode】142:环形链表,你的入口在哪? - 知乎
3.2 、计算环的长度
第一种方法是利用上面求出的环入口,再走一圈, 当node 和入口点相同时则走过了一圈,就可以求出长度,代码如下
int getlen(ListNode head) {
ListNode cycleIn = getCycleEnter(head); //得到环的入口
ListNode walker = cycleIn;
int len = 1;
while(walker->next != cycleIn)
{
++len;
walker = walker->next;
}
return len ; //直到找到相同指向的node,这个就是入口
}
第二种方法是当快慢指针相遇时,继续移动直到第二次相遇,此时快指针移动的距离正好比慢指针多一圈
int cycleLen(ListNode* head)
{
ListNode× slow = head;
ListNode× fast = head;
while(fast != null && fast->next != null )
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
break;
}
int len = 0;
// 找到相遇点 slow == fast,再走一圈相遇即为环长度
while(fast!= null && fast->next!= null)
{
++len;
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
break;
}
return len;
}
具体参考:每天一道LeetCode-----判断链表是否有环,如果有,找到环的入口位置_一个程序渣渣的小后院的博客-CSDN博客_链表判断是否有环
4、判断链表是否相交,并且求出第一个交点
思路:1、如果两个链表相交,那么它们一定有相同的尾结点,遍历两个链表,找出尾结点,如果尾结点相同,那么这两个链表相交,反之不相交
2.使用栈。
我们可以从头遍历两个链表。创建两个栈,第一个栈存储第一个链表的节点,第二个栈存储第二个链表的节点。每遍历到一个节点时,就将该节点入栈。两个链表都入栈结束后。则通过top判断栈顶的节点是否相等即可判断两个单链表是否相交。因为我们知道,若两个链表相交,则从第一个相交节点开始,后面的节点都相交。
若两链表相交,则循环出栈,直到遇到两个出栈的节点不相同,则这个节点的后一个节点就是第一个相交的节点。
void findnode(Node* head1, Node* head2) {
Stack<Node> stack1=new Stack<>();
Stack<Node> stack2=new Stack<>();
while(head1 != NULL) {
stack1.push(head1);
head1 = head1->next;
}
while(head2 != NULL) {
stack2.push(head2);
head2 = head2->next;
}
node temp=NULL; //存第一个相交节点
while(!stack1.empty()&&!stack1.empty()) //两栈不为空
{
temp=stack1.top();
stack1.pop();
stack2.pop();
if(stack1.top()!=stack2.top())
{
break;
}
}
return temp
}
这个方法在没有要求空间复杂度的时候,使用栈来解决这个问题也是挺简便的。
3、java中HashSet<> 可以判断是否包含元素。可以用两个HashSet,set1 和 set2 来存放链表的node,每一次新加node之前判断在另一个set中是否已经包含了,如果有则证明链表相交,同时这个就是交点
void CrossLink(Li)
boolean CrossLink(ListNode head1, ListNode head2) {
Set<ListNode> set1 = new HashSet<>();
Set<ListNode> set2 = new HashSet<>();
while (head1 != null && head2 != null) {
if(set1.contain(head2))
return true;
if(set2.contain(head1))
return true;
head1 = head1->next
head2 = head2->next
}
return false;
}
5、两个有序链表合并
leetcodeb 21:力扣
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2)
{
// l1、l2至少一个为空
if(!l1 || !l2)
{
// (l1 && !l2)为true表示l2为空,则返回l1,否则返回l2
// 返回l2的情况:(1)l1为空,l2不空;(2)l1、l2都为空。
return l1 && !l2 ? l1 : l2;
}
//声明一个头结点,赋初值为-1,这个值没啥用
ListNode* preHead = new ListNode(-1);
//记录需要插入的上一个节点,方便插入
ListNode* prev = preHead;
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val)
{
prev->next = l1;
l1 = l1->next;
} else {
prev->next = l2;
l2 = l2->next;
}
// 经过if之后,当前节点指向了prev->next,
//将 prev->next 赋值为下一个需要被插入的前几点
prev = prev->next;
}
// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
prev->next = l1 == nullptr ? l2 : l1;
// preHead->next 指向的才是第一个节点的位置
return preHead->next;
}
// 使用递归
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
} else if (l2 == null) {
return l1;
} else if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
6、链表去重
有序链表:
struct ListNode* deleteDuplicates(struct ListNode* head) {
if (!head) {
return head;
}
struct ListNode* cur = head;
while (cur->next) {
if (cur->val == cur->next->val) {
cur->next = cur->next->next;
} else {
cur = cur->next;
}
}
return head;
}
无序链表:
//删除方法二:进行双重循环遍历,外循环当前遍历的结点为cur,内循环从cur开始遍历,相同则删除
//时间复杂度O(N2),空间复杂度O(1)
public void removeDuplicatedElements_1(Node head) {
Node pcur = head; //从第一个节点开始向后遍历
Node pre = NULL; //前一节点
Node pnext = null; //后面的节点,每次判断的是pcur 和next的节点值是否相同
while (pcur != NULL) {
pre = pcur;
pnext = pcur.next;
//pnext 指针遍历链表
while (pnext != NULL) {
//如果节点值相同,需要删除 pnext 节点,则上一节点pre.next 的指向变为 pnext.next 的地址,删除pnext节点
if (pcur.data == pnext.data) {
pre.next = pnext.next;
// free(pnext); //释放内存
} else {
pre = pnext;
}
pnext = pnext.next;
}
pcur = pcur.next;
}
}
// 使用set
//时间复杂度O(N),空间复杂度O(N)
public void removeDuplicatedElements_2(Node head) {
if (NULL == head)
return;
HashSet<Integer> hash_set = new HashSet<Integer>();
Node pre = head;
Node cur = head.next;
hash_set.add(head.data);
while (NULL != cur) {
if (hash_set.contains(cur.data)) {
pre.next = cur.next;
} else {
hash_set.add(cur.data);
pre = cur;
}
cur = cur.next;
}
}
7 、链表中倒数第 K 个节点
【剑指offer】面试题22
注意,走了多少步就是过了多少距离,距离和长度不一样,比如以下这个例子的链表长度为n,但是从头节点走到n节点要走n-1步,也就是头节点到尾节点的距离是n-1。
思路1:假设整个链表有N个结点,那么倒数第k个结点就是从头结点开始的第n-k-1个结点。如果我们只要从头结点开始往后走n-k+1步就可以了。如何得到节点数n?这个不难,只需要从头开始遍历链表,每经过一个结点,计数器加1就行了。这样需要遍历两边链表
思路2:定义两个指针。第一个指针从链表的头指针开始遍历向前走k-1。第二个指针保持不动;从第k步开始,第二个指针也开始从链表的头指针开始遍历。由于两个指针的距离保持在k-1,当第一个(走在前面的)指针到达链表的尾指结点时,第二个指针正好是倒数第k个结点。
鲁棒性判断:
1、输入Head指针为null。由于代码会试图访问空指针指向的内存,程序会崩溃。
2、输入以Head为头结点的链表的结点总数少于k。由于在for循环中会在链表向前走k-1步,仍然会由于空指针造成崩溃。
3、输入的参数k为0或负数,同样会造成程序的崩溃。
public ListNode getKthFromEnd(ListNode head, int k){
// 鲁棒性的体现1:输入参数的检查
if(head == null || k <= 0){
return null;
}
// 定义两个指针
ListNode slow = head;
ListNode fast = head;
// 先走 k-1 步
for (int i = 0; i < k - 1; i++) {
if(fast.next != null){
// 遍历链表,直到找到尾节点,这样就可以直到链表中总的节点个数了
fast = fast.next;
}else{
// 鲁棒性的体现2:链表中总的节点数小于k,直接返回null
return null;
}
}
// 此时fast指针走了k-1步,后面两个指针一起向后遍历
// 当fast到达尾节点的时候,slow正好到达倒数第k个节点
while(fast.next != null){
slow = slow.next;
fast = fast.next;
}
return slow;
}
更简洁一点的写法:
public ListNode getKthFromEnd(ListNode head, int k){
if(head == null || k <= 0){
return null;
}
// 定义两个指针
ListNode slow = head;
ListNode fast = head;
for (int i = 0; fast != null; i++) {
if(i >= k){
// 此时fast指针走了k-1步,后面两个指针一起向后遍历
// slow 指针从head
slow = slow.next;
}
fast = fast.next;
}
// 考虑鲁棒性
// return i < k ? null : slow;
if( i < k) {
return null;
} else {
return slow;
}
}
8、链表快速排序
单链表的快速排序
首先,很容易想到的是:
1. 要做一轮基准值定位,怎么做?
2. 要做左子链表和右子链表的递归,怎么做?
第二个问题比较好回答,只要知道子链表的首尾节点,就可以做递归了。伪代码是:
void quick_sort_link(Node *start, Node *end=NULL);
第一个问题才是要解决的难题。思路如下:
假设第一轮基准值定位做完了,我们需要有什么才能继续进行?
很显然,需要有左子链表和右子链表的各自的首尾节点。那么,左链表的首节点和右链表的尾节点,这2个一开始就有了。所以,需要有的是:左子链表的尾节点,和 右子链表的首节点。而这2个节点分别位于基准值节点的左边和右边。
这个时候,有一个思路是:使用2个辅助指针 p1 和 p2.
p1 负责维护左子链表,它是左子链表的最后一个节点;
p2 负责维护右子链表,它不断右移:其实,相当于p2在不断扩充右子链表,而待探索区不断缩小
当p2在探索区发现大值的时候,只需右移即可,将其纳入右子链表的范围;
当p2发现小值的时候,就要把p1右移一个(相当于扩大左子链表的范围),然后交换p1和p2的值(把小值和原来右子链表的最后一个节点交换),然后p2继续右移。
到最后,循环结束的时候,还需要交换基准值(pstart)和p1的值,因为,基准值从来没有动过,还在第一个节点的位置,而p1最终已经指向左子链表的最后一个位置,因此需要交换它们2个。
/*
基准值是start->data;
将原链表看作2个链表:左链表和右链表,左链表最后一个节点就是基准值
p1是左链表的最后一个节点,p2是右链表的最后一个节点
因此,当遇到大于基准值的时候,p2一直右移;
当遇到小于基准值的时候,p1右移一个,再交换p1和p2的值,相当于维持了p1和p2的定义
一轮循环的最后,p2到达了end的位置,此时,应该交换p1和start节点的值,这时才是真正的一轮处理的结束
下一轮,就递归调用 qs(start, p1) 和 qs(p1->next, end) 了。
*/
void quick_sort_list(Node* start, Node* end=NULL)
{
if (start == NULL || start == end) return;
int key = start->data
Node* p1 = start;
Node* p2 = start->next;
while (p2 != end) {
if (p2->data < key ) {
p1 = p1->next;
swap(p1->data, p2->data);
}
p2 = p2->next;
}
swap(p1->data, start->data);
quick_sort_list(start, p1);
quick_sort_list(p1->next, end);
}
//使用快慢指针,得到分组后基准点的pnode
Node* GetPartion(Node* pstart, Node* pEnd)
{
int key = pBegin->key; //做基准的数值
Node* slow = pstart;
Node* fast = slow->next;
while(fast != pEnd)
{
if(fast->key < key)
{
slow = slow->next;
swap(slow->key, fast->key);
}
fast = fast->next;
}
swap(slow->key, pstart->key); //基准元素归为
return slow;
}
void QuickSort(Node* pstart, Node* pEnd)
{
if(pBeign != pEnd)
{
Node* pNode = GetPartion(pstart,pEnd);
QuickSort(pstart, pNode);
QuickSort(partion->next, pNode);
}
}
参考:单链表的快速排序与归并排序_执假以为真的博客-CSDN博客_单链表的快速排序
LeetCode:归并排序:力扣
排序链表「八大排序算法」 力扣