【聲明】
歡迎轉載,但請保留文章原始出處→_→
文章來源:http://www.lxweimin.com/p/08d085b34b2c
聯系方式:zmhg871@gmail.com
【正文】
鏈表是非常基礎和靈活的數據結構,在面試中出現的頻率非常高。以下是我在學習《劍指offer》過程中對鏈表問題的總結,希望對大家復習有幫助。
(Java實現)
【目錄】
- 單鏈表的創建和遍歷
- 求單鏈表中節點的個數
- 查找單鏈表中的倒數第k個結點
- 查找單鏈表中的中間結點
- 合并兩個有序的單鏈表,合并之后的鏈表依然有序
- 反轉鏈表
- 從尾到頭打印單鏈表
- 刪除鏈表結點
【提示】
當我們用一個指針遍歷鏈表不能解決問題的時候,可以嘗試用兩個指針來遍歷鏈表,可以讓其中一個指針遍歷的速度快一些(比如一次在鏈表中走兩步),或者讓它先在鏈表上走幾步。
- 求鏈表的倒數K個節點
- 求鏈表的中間節點
- 求鏈表中是否有環
- 單鏈表的創建和遍歷:
public class LinkList {
//Linked List Node
static class LinkNode{
int value; // value for this node
LinkNode next = null; // Pointer to next node
LinkNode(int data){
value = data;
}
LinkNode(){}
}
private LinkNode header = null;
//方法:向鏈表中添加數據
public void add(int value){
if(header == null) header = new LinkNode();
LinkNode node = new LinkNode(value);
node.next = header.next;
header.next = node;
}
//方法:遍歷鏈表
public void print(){
if(header == null) return;
LinkNode temp = header.next;
while(temp != null){
System.out.println("value : " + temp.value);
temp = temp.next;
}
}
public static void main(String[] args) {
LinkList list = new LinkList();
//向LinkList中添加數據
for (int i = 0; i < 7; i++) {
list.add(i);
}
list.print();
System.out.println(list.getSize());
}
}
- ****求單鏈表中節點的個數:時間復雜度為O(n)****
//方法:獲取單鏈表節點的個數
public int getSize(){
if(header == null) return 0;
LinkNode temp = header.next;
int size = 0;
while(temp != null){
size++;
temp = temp.next;
}
return size;
}
- 查找單鏈表中的倒數第k個結點
題目描述:輸入一個單向鏈表,輸出該鏈表中倒數第k個節點,鏈表的倒數第1個節點為鏈表的尾指針。 (劍指offer,題15)
3.1 普通思路:(遍歷鏈表2次)
首先計算出鏈表的長度size,然后輸出第(size-k)個節點就可以了。
(注意鏈表為空,k為0,k大于鏈表中節點個數時的情況)。
public int getLastNode(int index){
if(header == null || index == 0) return -1;
int size = getSize();
if(index > size) return -1;
LinkNode temp = header.next;
for(int i =1;i<= size-index;i++){
temp = temp.next;
}
return temp.value;
}
3.2 改進思路:(遍歷鏈表1次)
聲明兩個指針:first和second,首先讓first和second都指向第一個結點,然后讓first結點往后挪k-1個位置,此時first和second就間隔了k-1個位置,然后整體向后移動這兩個節點,直到first節點走到最后一個結點的時候,此時second節點所指向的位置就是倒數第k個節點的位置。
public int getLastNode(int k){
if(header == null || k<= 0) return -1;
LinkNode first = header;
LinkNode second = header;
//讓first結點往后挪k-1個位置
for(int i =0;i<k-1;i++){
if(first.next != null){
first = first.next;
}else{
return -1;
}
}
while(first.next != null){
first = first.next;
second = second.next;
}
return second.value;
}
- 查找單鏈表中的中間結點
題目描述:求鏈表的中間節點,如果鏈表的長度為偶數,返回中間兩個節點的任意一個,若為奇數,則返回中間節點。
4.1 普通思路:(遍歷鏈表2次)
首先計算出鏈表的長度size,然后輸出第(size/2)個節點就可以了。
public int getMiddleNode(){
if(header == null)
return -1;
//第1次遍歷獲取節點數
LinkNode temp = header.next;
int size = 0;
while(temp != null){
size++;
temp = temp.next;
}
if(size == 0) return -1;
//第2次遍歷查找中間節點
temp = header.next;
for(int i=0;i<size/2;i++){
temp = temp.next;
}
return temp.value;
}
4.2 改進思路:(遍歷鏈表1次)
聲明兩個指針:first和second,同時從鏈表頭節點開始,一個指針每次移動兩步,另一個每次移動一步,當走的快的指針走到鏈表的末尾時,走的慢的指針正好在鏈表的中間。
public LinkNode getMiddleNode(){
if(header == null)
return null;
LinkNode first = header; //快指針
LinkNode second = header;
while(first!=null && first.next!=null){
first = first.next.next;
second = second.next;
}
return second;
}
- 合并兩個有序的單鏈表,合并之后的鏈表依然有序
題目描述:輸入兩個遞增排序的鏈表,合并這兩個鏈表并使新鏈表中的結點仍然是按照遞增排序的。 (劍指offer,題17)
例如:
鏈表1: 1->3->5->7
鏈表2: 2->4->6->8
合并之后:1->2->3->4->5->6->7->8
解題思路 : 類似于歸并排序
首先分析合并兩個鏈表的過程。鏈表1的頭結點的值小于鏈表2的頭結點的值,因此鏈表1的頭結點將是合并后鏈表的頭結點。
我們繼續合并兩個鏈表中剩余的結點。在兩個鏈表中剩下的結點依然是排序的,因此合并這兩個鏈表的步驟和前面的步驟是一樣的。我們還是比較兩個頭結點的值。此時鏈表2的頭結點的值小于鏈表1的頭結點的值,因此鏈表2的頭結點的值將是合并剩余結點得到的鏈表的頭結點。我們把這個結點和前面合并鏈表時得到的鏈表的尾節點鏈接起來。
當我們得到兩個鏈表中值較小的頭結點并把它鏈接到已經合并的鏈表之后,兩個鏈表剩余的結點依然是排序的,因此合并的步驟和之前的步驟是一樣的。這就是典型的遞歸的過程,我們可以定義遞歸函數完成這一合并過程。
//兩個參數代表的是兩個鏈表的頭結點,返回合并后的頭結點
public static LinkNode Merge(LinkNode head1,LinkNode head2){
if(head1 == null) return head2;
if(head2 == null) return head1;
LinkNode mergeHead = null;
if(head1.value <= head2.value){
mergeHead = head1;
mergeHead.next = Merge(head1.next,head2);
}else{
mergeHead = head2;
mergeHead.next = Merge(head1,head2.next);
}
return mergeHead;
}
注意問題:
1)鏈表不能斷開,且仍為遞增順序;
2)代碼魯棒性,考慮鏈表為空的情況;
//遍歷的方式
public static LinkNode Merge(LinkNode head1,LinkNode head2){
//預判斷
if(head1 == null && head2 == null){
return null;
}
if(head1 == null){
return head2;
}
if(head2 == null){
return head1;
}
LinkNode head;//新的頭結點
LinkNode temp;
//確定新的頭結點
if(head1.value <= head2.value){
head = head1;
temp = head1;
head1 = head1.next;
}else{
head = head2;
temp = head2;
head2 = head2.next;
}
//合并
while(head1 != null && head2!=null){
if(head1.value <= head2.value){
temp.next = head1;
temp = temp.next;
head1 = head1.next;
}else{
temp.next = head2;
temp = temp.next;
head2 = head2.next;
}
}
//合并剩余的元素
if(head1 != null){
temp.next = head1;
}
if(head2 != null){
temp.next = head2;;
}
return head;
}
- 查找單鏈表中的倒數第k個結點
題目描述:輸入一個單鏈表的頭結點,反轉該鏈表并輸出反轉后的頭結點。 (劍指offer,題16)
例如:
鏈表反轉前: 1->3->5->7
鏈表反轉后: 7->5->3->1
解題思路 : 從頭到尾遍歷原鏈表,使用三個節點pNode、pPrev、pPost 記錄當前節點,前一個節點和后一個節點。
public static LinkNode ReverseList(LinkNode header){
if(header == null) return null;
LinkNode pNode = header;
LinkNode pPrev = null; //記錄前一個節點
LinkNode pPost = null; //記錄后一個節點
while(pNode != null){
pPost = pNode.next;
pNode.next = pPrev;
pPrev = pNode;
pNode = pPost;
}
return pPrev;
}
注意問題:鏈表為空和只有1個節點的問題。
- ****從尾到頭打印單鏈表****
題目描述:輸入一個鏈表的頭結點,從尾到頭反過來打印出每個節點的值. (劍指offer,題5)
7.1 解法1:在允許修改鏈表的結構的情況下,可以先反轉鏈表,然后從頭到尾輸出。
7.2 解法2:在不允許修改鏈表的結構的情況下,可以使用棧實現。
遍歷的順序是從頭到尾的順序,可輸出的順序卻是從尾到頭。也就是說第一個遍歷到的節點最后一個輸出,而最后一個遍歷到的節點第一個輸出。這就是典型的“后入先出”,我們可以用棧來實現這種順序。
public static void reversePrint(LinkNode header){
if(header == null) return;
Stack<LinkNode> stack = new Stack<>();
LinkNode temp = header;
while(temp!=null){
stack.push(temp);
temp = temp.next;
}
while(!stack.empty()){
System.out.println(stack.pop().value);
}
}
7.3 解法3:在不允許修改鏈表的結構的情況下,可以使用遞歸實現。(遞歸的本質上就是一個棧結構)
要想實現反過來輸出鏈表,每訪問一個節點的時候,先遞歸輸出它后面的節點,再輸出節點自身,這樣鏈表的輸出結果就反過來了。
public static void reversePrint(LinkNode header){
if(header == null) return;
reversePrint(header.next);
System.out.println(header.value);
}
代碼簡潔,但是鏈表非常長的情況下可能會導致函數調用棧溢出。所以顯示使用棧基于循環實現的代碼的魯棒性會更好。
- 刪除鏈表結點
題目描述:給定鏈表的頭指針和一個節點指針,在O(1)時間刪除該節點。 (劍指offer,題13)
8.1 普通思路:平均時間復雜度O(n)
從鏈表的頭結點開始,順序遍歷要刪除的節點,并在鏈表中刪除該節點。
public void delete(LinkNode head,LinkNode toBeDeleted){
if(head == null || toBeDeleted == null)
return;
LinkNode prev = head;
LinkNode temp = head.next;
while( temp != null ){
if(temp.value == toBeDeleted.value){
prev.next = temp.next;
break;
}else{
temp = temp.next;
prev = prev.next;
}
}
}
8.2 改進思路:平均時間復雜度O(1)
前一種方法之所以要從頭開始查找,是因為我們需要得到被刪除節點的前一個節點,但是想要刪除節點并不一定非要找到前一個節點。由于在單鏈表中可以很方便的得到刪除節點的下一個節點,如果我們把下一個節點的內容復制到需要刪除的節點上覆蓋原有的內容,再把下一個節點刪除,就相當于把需要刪除的節點刪除了。
public class Test {
/**
* 鏈表結點
*/
public static class ListNode {
int value; // 保存鏈表的值
ListNode next; // 下一個結點
}
/**
* 【注意1:這個方法和文本上的不一樣,書上的沒有返回值,這個因為JAVA引用傳遞的原因,
* 如果刪除的結點是頭結點,如果不采用返回值的方式,那么頭結點永遠刪除不了】
* 【注意2:輸入的待刪除結點必須是待鏈表中的結點,否則會引起錯誤,這個條件由用戶進行保證】
*
* @param head 鏈表表的頭
* @param toBeDeleted 待刪除的結點
* @return 刪除后的頭結點
**/
public static ListNode deleteNode(ListNode head,ListNode toBeDeleted){
//預判斷
if(head == null)
return null;
if(toBeDeleted == null){
return head;
}
// 如果刪除的是頭結點,直接返回頭結點的下一個結點
if(head.value == toBeDeleted.value){
return head.next;
}
// 在多個節點的情況下,如果刪除的是最后一個元素
if(toBeDeleted.next == null){
// 找待刪除元素的前驅
ListNode temp = head;
while(temp.next.value != toBeDeleted.value){
temp = temp.next;
}
// 刪除待結點
temp.next = null;
}else{
// 在多個節點的情況下,如果刪除的是某個中間結點
toBeDeleted.value = toBeDeleted.next.value;
toBeDeleted.next = toBeDeleted.next.next;
}
// 返回刪除節點后的鏈表頭結點
return head;
}
/**
* 輸出鏈表的元素值
*
* @param head 鏈表的頭結點
*/
public static void printList(ListNode head) {
while (head != null) {
System.out.print(" value :" + head.value);
head = head.next;
}
System.out.println();
}
public static void main(String[] args) {
ListNode head1 = new ListNode();
head1.value = 1;
ListNode head2 = new ListNode();
head2.value = 2;
ListNode head3 = new ListNode();
head3.value = 3;
ListNode head4 = new ListNode();
head4.value = 4;
head1.next = head2;
head2.next = head3;
head3.next = head4;
printList(head1);
ListNode head = head1;
// 刪除頭結點
head = deleteNode(head, head1);
printList(head);
// 刪除尾結點
head = deleteNode(head, head4);
printList(head);
// 刪除中間結點
head = deleteNode(head, head3);
printList(head);
ListNode node = new ListNode();
node.value = 12;
// 刪除的結點不在鏈表中
head = deleteNode(head, node);
printList(head);
}
}
考察思維創新能力,打破常規。當我們需要刪除一個節點時,并不一定要刪除這個節點本身,可以先把下一個節點的內容復制過來覆蓋原來需要被刪除節點的內容,然后把下一個節點刪除。
參考資料:
- 劍指offer
- 劍指 Offer 學習心得
- 鏈表面試題Java實現
[2015-9-10]