(一)找到链表的倒数第n个节点
1.这个问题不难,我们可以两次遍历链表,第一遍记录链表的长度L,第二遍从头遍历链表,找到L-n+1的位置的节点,然后输出,这样时间复杂度为o(n),空间复杂度为o(1)
2.上面的方法看起来不错,但是我们还可以有更好的方法,我们仅仅使用一次遍历就可以,我们使用两个指针slowNode和fastNode来一起遍历,两个指针都指向链表头结点,仅当fastNode移动了n个节点的时候,slowNode才开始移动,然后两个指针同时移动直到fastNode到达链表末尾,这个时候slowNode所指向的节点就是倒数第n个节点,时间复杂度O(n),空间复杂度o(1)
public ListNode NthNodeFromEnd(ListNode headNode, int k){ ListNode slowNode = headNode; ListNode fastNode = headNode; for(int j = 1; j<k; j++){ //错误处理,当参数大于链表元素个数时 if(fastNode == null||fastNode.getNext() == null){ System.out.println("参数不正确"); return null; } fastNode = fastNode.getNext(); } while(fastNode.getNext()!=null){ fastNode = fastNode.getNext(); slowNode = slowNode.getNext(); } return slowNode; }
(二)判断链表是以NULL结尾还是形成一个环
1.一个直接的判定方法是从第一个节点开始,令其为当前节点,看看后面是否存在节点的后继节点等于当前节点,如果存在这样的节点就说明存在环,如果不存在则对链表中的其他节点重复上述操作。但是这样不仅复杂,如果处理不好还容易出现死循环。
2.一个简单有效的方法是由floyd提出来的,,该方法被称为floyd环判定算法,该方法使用链表中移动速度不同的两个指针,一旦他们进入环就会相遇,即表示存在环,这个判定方法的正确性在于,快速移动指针和慢速移动指针将会指向同一位置的唯一可能情况,就是整个或部分链表是一个环。时间复杂度O(n),空间复杂度O(1)
public Boolean DoesLinkedListContainsLoop(ListNode headNode){ ListNode fastNode = headNode; ListNode slowNode = headNode; while(fastNode.getNext()!=null&&fastNode.getNext().getNext()!=null){ slowNode = slowNode.getNext(); fastNode = fastNode.getNext().getNext(); if(slowNode == fastNode){ return true; } } return false; }
(三)判定链表当中是否存在环,如果存在找到环的起始节点
这是问题二的扩展,首先判断是否存在环,如果存在环,则在fastNode与slowNode相遇之后,初始化slowNode使其等于头结点,然后slowNode与fastNode从各自位置沿着链表移动,每次移动一个节点,他们再次相遇的位置就是环开始的位置,我们也可以用这种方法来删除环。
假设起点到环的起点距离为m,已经确定有环,环的周长为n,(第一次)相遇点距离环的起点的距离是k。那么当两者相遇时,慢指针移动的总距离为i,i = m + a * n + k,因为快指针移动速度为慢指针的两倍,那么快指针的移动距离为2i,2i = m + b * n + k。其中,a和b分别为慢指针和快指针在第一次相遇时转过的圈数。我们让两者相减(快减慢),那么有i = (b - a) * n。即i是圈长度的倍数。利用这个结论我们就可以理解Floyd解法为什么能确定环的起点。将一个指针移到链表起点,另一个指针不变,即距离链表起点为i处,两者同时移动,每次移动一步。当第一个指针前进了m,即到达环起点时,另一个指针距离链表起点为i + m。考虑到i为圈长度的倍数,可以理解为指针从链表起点出发,走到环起点,然后绕环转了几圈,所以第二个指针也必然在环的起点。即两者相遇点就是环的起点。
public ListNode FindBiginOfLoop(ListNode headNode){ Boolean loopExists = false; ListNode fastNode = headNode; ListNode slowNode = headNode; while(fastNode.getNext()!=null&&fastNode.getNext().getNext()!=null){ slowNode = slowNode.getNext(); fastNode = fastNode.getNext().getNext(); if(slowNode == fastNode){ loopExists = true; break; } } if(loopExists){ slowNode = headNode; while(true){ if(slowNode == fastNode){ return slowNode; } slowNode = slowNode.getNext(); fastNode = fastNode.getNext(); } }else{ System.out.println("不存在环"); return null; } }
(四)判断链表当中是否存在环,如果存在返回环的长度
嗯,还是第二个问题的延伸,如果存在环的话,记录slowNode当时指向的节点,然后继续让slowNode每次移动一步,直到再次到达记录的位置就可以计算出环的长度
public int FindLoopLength(ListNode headNode){ Boolean loopExists = false; ListNode fastNode = headNode; ListNode slowNode = headNode; while(fastNode.getNext()!=null && fastNode.getNext().getNext()!=null){ slowNode = slowNode.getNext(); fastNode = fastNode.getNext().getNext(); if(slowNode == fastNode){ loopExists = true; break; } } if(loopExists){ int count = 1; while(slowNode.getNext()!= fastNode){ slowNode = slowNode.getNext(); count++; } return count; }else{ return 0; } }
(五)在有序链表中插入一个新的节点
遍历链表,找到合适的位置插入链表节点,要注意插入到链表头结点时要做一些处理
public ListNode InsertInSortedList(ListNode headNode, ListNode NewNode){ ListNode currentNode = headNode; ListNode tempNode = null; if(headNode == null) return NewNode; if(currentNode.getData() >= NewNode.getData()){ NewNode.setNext(currentNode); return NewNode; } while(currentNode!=null && currentNode.getData()<NewNode.getData()){ tempNode = currentNode; currentNode = currentNode.getNext(); } NewNode.setNext(currentNode); tempNode.setNext(NewNode); return headNode; }
(六)逆置单向链表
遍历链表,将链表方向反转即可,但是注意,原来链表头部节点,反转后要指向null;时间复杂度为o(n),空间复杂度为O(1)
public ListNode ReverseList(ListNode headNode){ ListNode tempNode = null; ListNode nextNode = null; while(headNode!=null){ nextNode = headNode.getNext(); headNode.setNext(tempNode); tempNode = headNode; headNode = nextNode; } return tempNode; }
(七)假设两个链表在某个节点相交后成为一个单向链表,两个链表头结点是已知的,相交节点未知,求出链表相交的节点
1. 将两个链表分别放到栈里,然后同步出栈,最后一个相等的节点就是相交节点,这个比较简单,时间复杂度为o(max(m,n))
空间复杂度为O(m+n)
2. 可以先分别遍历原来链表,计算链表长度,求出链表长度的差d,长链表先走d步,然后长链表和短链表一起向后遍历,出现的第一个相等的字节就是链表相交的地方,时间复杂度为O(max(m,n)),空间复杂度为O(1),比前一种做法空间复杂度低
public ListNode FindIntersectingNode(ListNode list1, ListNode list2){ int length1 = ListLength(list1); int length2 = ListLength(list2); ListNode Node1 = null , Node2 = null; int diff = 0; if(length1>length2){ Node1 = list1; Node2 = list2; diff = length1-length2; }else{ Node1 = list2; Node2 = list1; diff = length2 - length1; } while(diff>0){ Node1 = Node1.getNext(); diff--; } while(Node1!=null){ if(Node1.getData()==Node2.getData()){ return Node1; } Node1 = Node1.getNext(); Node2 = Node2.getNext(); } return null; }
(八)如何找到链表的中间节点
1.遍历一遍链表,返回链表长度n,再遍历一遍找到n/2的位置,但是这样需要遍历两遍链表
2.使用两个指针,一个快指针一个慢指针,快指针移动的速度是慢指针移动速度的两倍,当快指针遍历结束时慢指针的位置就是链表中间位置,当节点个数为奇数时四舍五入
public ListNode FindMiddle(ListNode headNode){ ListNode slowNode = headNode, fastNode = headNode; if(ListLength(headNode)==0){ return null; } while(fastNode.getNext()!=null&&fastNode.getNext().getNext()!=null){ slowNode = slowNode.getNext(); fastNode = fastNode.getNext().getNext(); } return slowNode; }
(九)如何从表尾开始打印链表
使用递归超级方便,空间复杂度O(n),时间复杂度为O(n)
public void PrintListFromEnd(ListNode headNode){ if(headNode == null) return; PrintListFromEnd(headNode.getNext()); System.out.println(headNode.getData()); }