算法單解之迴文單向鏈表的3種解法

題目解析

題目簡述:判斷一個單向鏈表是不是迴文鏈表 題目簡析在解題以前,咱們首先須要理解題目的含義。首先須要理解什麼是迴文鏈表。迴文一詞最初出如今文學做品中,下面是維基百科給出的迴文的定義。面試

迴文,亦稱迴環,是正讀反讀都能讀通的句子,亦有將文字排列成圓圈者,是一種修辭方式和文字遊戲。算法

上面定義可能不太容易理解,簡單的能夠理解爲正讀和反讀都同樣的句子。爲了便於理解下面給出幾個簡單例子。數組

上海自來水來自海上 黃山落葉松葉落山黃bash

迴文鏈表的概念與此相似,也就是正想和反向遍歷,序列同樣的鏈表。如圖1是一個迴文單向鏈表的具體示例,該鏈表若是進行正向遍歷結果爲1->2->3->2->1。若是進行反向遍歷,則結果仍然爲1->2->3->2->1。所以,咱們認爲這個鏈表是迴文鏈表。 數據結構

圖1 迴文單向鏈表

從圖中能夠看出,若是分別從首尾進行遍歷,而且元素值相等的話,那麼能夠斷定這個鏈表是迴文鏈表。具體如上圖暗灰色虛線表示的對應關係。若是存儲數據的數據結構是雙向鏈表或者數組的話,那麼問題就很容易解決,可是問題在於本題要求的是單向鏈表。所以,這就限制了咱們遍歷的方向只能從頭至尾,而沒法反向遍歷。函數

常規解題思路

根據前面對題目的分析,咱們很容易想出一個常規的解題思路。那就是將上述單向鏈表的數據存儲在數組當中,而後再斷定數組中的數據是否爲迴文。若是數組中的數據是迴文,那就能夠斷定單向鏈表是迴文鏈表了(將原有數據結構轉換爲方便解題的其它數據結構是解決算法問題的常見方法,後續還有相似的題目)。 有了上面的解題思路,咱們能夠很容易的寫出以下代碼實現(C語言)。整個代碼邏輯分爲3步,分別以下:優化

  1. 計算單向鏈表的長度
  2. 根據鏈表長度分配數組的存儲空間,並經過鏈表初始化數組
  3. 根據數組元素判斷鏈表是否爲迴文
bool isPalindrome(struct ListNode* head){
    int len = 0;
    int index = 0;
    int mid = 0;
    struct ListNode* cur = NULL;
    int* data = NULL;
    
    //計算鏈表的長度
    cur = head;
    while(cur) {
        len ++;        
        cur = cur->next;
    }
    
    //分別數組空間,將鏈表轉換爲數組
    data = malloc(len * sizeof(int));
    if (data == NULL) {
        return false;
    }
    memset(data, 0, len * sizeof(int));
    
    cur = head;
    index = 0;
    while(cur) {
        data[index] = cur->val;
        cur = cur->next;
        index ++;
    }
    
    //根據數組內容判斷是否爲迴文
    mid = len / 2;
    for (int i = 0; i< mid; i++) {
        if (data[i] != data[len-i-1]) {
            return false;
        }
    }
    
    return true; 
}
複製代碼

時間複雜度的優化

上述算法雖然邏輯清晰,但有2個缺點,一個是須要分配等量的輔助存儲空間,另一個是須要遍歷2次鏈表,及一次數組遍歷。所以,不管是在空間複雜度仍是時間複雜度上都不能說是最優的。這種狀況下,面試官可能不會滿意目前的答案。 觀察鏈表的內容咱們能夠看出前半部分與後半部分的內容的順序正好是相反的。若是咱們把前半部分壓棧,並在遍歷後半部分的時候逐個出棧,那麼正好能夠實現前半部分和後半部分的對比。 ui

圖2 基於棧的實現
這裏面關鍵的一點是找到鏈表的中間位置,具體方法能夠使用快慢指針的方法。也就是快指針每次走2步,慢指針每次走1步,這樣當快指針走到結尾的時候,慢指針正好在鏈表的中間位置。 這裏須要注意邊界條件,也就是鏈表節點數量爲奇數和偶數時的處理上要注意,避免錯誤。 因爲C語言自己沒有棧這種數據結構,所以咱們用C++中的標準庫實現該函數,具體代碼以下:

class Solution {
public:
    bool isPalindrome(ListNode* head) {
         stack<ListNode*>stack;
		 ListNode* slow=head;
		 ListNode* fast=head;
		 
         //0個節點或是1個節點
		 if(fast==NULL||fast->next==NULL)
			 return true;
         //將鏈表的前半部分入棧
		 stack.push(slow);
		 while(fast->next!=NULL&&fast->next->next!=NULL)
		 {			
			 fast = fast->next->next;
			 slow = slow->next;
			 stack.push(slow);
		 }
        
         //鏈表長度爲偶數
		 if(fast->next!=NULL)
			 slow = slow->next;
		 
        //從鏈表後半部分開始,逐個出棧,並與鏈表的後半部分進行對比
		 ListNode* cur=slow;
		 while(cur != NULL)
		 {
             ListNode* sp = stack.top();
             stack.pop();
			 if(cur->val != sp->val)
				 return false;
			 cur = cur->next;
		 }
		 return true;
    }   
};
複製代碼

上述算法只對鏈表遍歷了一次,相對前一種方法在時間複雜度方面作了很大的優化。spa

存儲空間的優化

上面解法比較直觀,但最大的問題是須要額外的存儲空間。若是迴文鏈表的數據量較大,上述解法的效率就差不少。或者面試官要求不可以使用太多輔助空間,那麼上述解決方法就不知足面試官的要求。 鏈表反轉你們應該都有所瞭解,這個也是一個常見的面試題。所以,咱們能夠將鏈表的前半部分或者後半部分進行反轉操做,而後進行對比便可。以下是該算法的C語言實現。指針

struct ListNode* reverse(struct ListNode* head){
    
    if(!head){
        return NULL;
    }
    struct ListNode *pre = NULL;
    struct ListNode *cur = head;
    struct ListNode *next = NULL;
    
    while(cur){
        next = cur->next;
        cur->next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
}
 
bool isPalindrome(struct ListNode* head) {
    
    if(head == NULL || head->next == NULL)
        return true;
    
    struct ListNode *fast = head , *slow = head;
    
    /*經過如下程序,快指針fast指向鏈表最後一個結點(奇數)或者倒數第二個結點(偶數)*/
    while(fast->next != NULL && fast->next->next != NULL){
        fast = fast->next->next;
        slow = slow->next;
    }
 
    slow = slow->next;
    slow = reverse(slow);  //將鏈表的後半段反轉
    
    /*將鏈表反轉後的後半段與前半段比較,奇數長度的話其實比較的是slow以前和以後的結點*/
    while(slow)
    {
        if(slow->val != head->val)         
        {
            return false;
        }
        slow = slow->next;
        head = head->next;
    }
    return true;
}
複製代碼

上述方法破壞了原始鏈表,你們考慮一下,若是不破壞原始鏈表,應該如何解決。

相關文章
相關標籤/搜索