面試算法知識梳理(3) - 字符串算法第二部分

面試算法代碼知識梳理系列

面試算法知識梳理(1) - 排序算法
面試算法知識梳理(2) - 字符串算法第一部分
面試算法知識梳理(3) - 字符串算法第二部分
面試算法知識梳理(4) - 數(shù)組第一部分
面試算法知識梳理(5) - 數(shù)組第二部分
面試算法知識梳理(6) - 數(shù)組第三部分
面試算法知識梳理(7) - 數(shù)組第四部分
面試算法知識梳理(8) - 二分查找算法及其變型
面試算法知識梳理(9) - 鏈表算法第一部分
面試算法知識梳理(10) - 二叉查找樹
面試算法知識梳理(11) - 二叉樹算法第一部分
面試算法知識梳理(12) - 二叉樹算法第二部分
面試算法知識梳理(13) - 二叉樹算法第三部分


一、概要

本文介紹了有關字符串的算法第二部分的Java代碼實現(xiàn),所有代碼均可通過 在線編譯器 直接運行,算法目錄:

  • 查找字符串中的最長重復子串
  • 求長度為N的字符串的最長回文子串
  • 將字符串中的*移到前部,并且不改變非*的順序
  • 不開辟用于交換的空間,完成字符串的逆序C++
  • 最短摘要生成
  • 最長公共子序列

二、代碼實現(xiàn)

2.1 查找字符串中的最長重復子串

問題描述

給定一個文本文件作為輸入,查找其中最長的重復子字符串。例如,"Ask not what your country can do for you, but what you can do for your country"中最長的重復字符串是“can do for you”,第二長的是"your country"

解決思路

這里解決問題的時候用到了 后綴數(shù)組 的思想,它指的是字符串所有右子集的集合,例如字符串abcde,它的后綴數(shù)組就為["abcde", "bcde", "cde", "de", "e"]

解法分為三步:

  • 求得輸入字符串p的后綴數(shù)組,把它存放在一個List當中,這里注意去掉空格的情況。
  • List中的所有元素進行快速排序。快速排序的目的不在于使得整個數(shù)組有序,而在于 使得前綴差異最小的兩個字符串在數(shù)組中位于相鄰的位置,對于上面的例子,其排序結果為:
后綴數(shù)組的快速排序結果
  • 遍歷排序后的數(shù)組,只需要對數(shù)組中的 相鄰的兩個元素 從頭開始比較,計算出這兩個字符串相同前綴的長度。遍歷之后,取得的最大值就是最長重復子串的長度,而這兩個字符串的相同前綴就是最長重復子串。

實現(xiàn)代碼

import java.util.ArrayList;
import java.util.List;
import java.lang.String;

class Untitled {

    static void quickSortStr(List<String> c, int start, int end){
        if(start >= end)
            return;
        int pStart = start;
        int pEnd = end;
        int pMid = start;
        String t = null;
        for (int j = pStart+1; j <= pEnd; j++) {
            if ((c.get(pStart)).compareTo(c.get(j)) > 0) {
                pMid++;
                t = c.get(pMid); 
                c.set(pMid, c.get(j)); 
                c.set(j, t);
            }
        }
        t = c.get(pStart); 
        c.set(pStart, c.get(pMid)); 
        c.set(pMid, t);
        quickSortStr(c, pStart, pMid-1);
        quickSortStr(c, pMid+1, pEnd);
    }
    
    //獲得兩個字符串從第一個字符開始,相同部分的最大長度。
    static int comLen(String p1, String p2){
        int count = 0;
        int p1Index = 0;
        int p2Index = 0;
        while (p1Index < p1.length()) {
            if (p1.charAt(p1Index++) != p2.charAt(p2Index++))
                return count;
            count++;
        }
        return count;
    }

    static String longComStr(String p, int length){
        List<String> dic = new ArrayList<String>();
        int ml = 0 ;
        for (int i = 0; i < length; i++) {
            if (p.charAt(i) != ' ') {
                //構造所有的后綴數(shù)組。
                dic.add(p.substring(i, p.length()));
            }
        }
        String mp = null;
        //對后綴數(shù)組進行排序。
        quickSortStr(dic, 0, dic.size()-1);
        //打印排序后的數(shù)組用于調(diào)試。
        for (int i = 0; i < dic.size(); i++) {
            System.out.println("index=" + i + ",data=" + dic.get(i));
        }
        for (int i = 0; i < dic.size()-1; i++) {
            int tl = comLen(dic.get(i), dic.get(i+1));
            if (tl > ml) {
                ml = tl;
                mp = dic.get(i).substring(0, ml);
            }
        }
        return mp;
    } 

    public static void main(String[] args) {
        String source = "Ask not what your country can do for you, but what you can do for your country";
        System.out.println("result = " + longComStr(source, source.length()));
    }
}

運行結果

>> result = can do for you

2.2 求長度為 N 的字符串的最長回文子串

問題描述

長度為N的字符串,求這個字符串里的最長回文子串,回文字符串 簡單來說就是一個字符串正著讀和反著讀是一樣的。

解決思路

這里用到的是Manacher算法,首先需要對原始的字符串進行預處理,即在每個字符之間加上一個標志位,這里用#來表示,這會使得對于任意一個輸入,經(jīng)過處理后的字符串長度為2*len+1,也就是說 處理后的字符串始終為奇數(shù)

在上面我們已經(jīng)介紹過,回文串中最左或最右位置的字符與其對稱軸的距離稱為 回文半徑Manacher定義了一個數(shù)組RL[i],它表示 i個字符為對稱軸的回文串最右一個字符與字符i的閉區(qū)間所包含的字符個數(shù),以google為例,經(jīng)過處理后的字符串為#g#o#o#g#l#e,那么RL[i]的值為:


RL[i]-1的值就是原始字符串中,以位置i為對稱軸的最長回文串的長度,那么接下來的問題就變成如何計算RL[i]數(shù)組。

首先,我們需要兩個輔助的變量maxidRmaxid,它表示當前計算的回文字符串中,所能觸及到的最右位置,而maxid則表示該回文串的對稱軸所在位置,而RL[maxid]為該回文串的距離。

假設我們此時遍歷到了第i個字符,那么這時候有兩種情況:

(1) i < maxidR

在這種情況下,我們知道p[maxid+1, .., maxid+RL[maxid]-1]p[maxid-1, .., maxid-RL[maxid]+1]部分是關于p[maxid]對稱的,利用這個有效信息,可以避免一些不必要的判斷。

現(xiàn)在,我們獲得i關于maxid的對稱點j,這個點位于maxid的左側(cè),因此,我們已經(jīng)計算過以它為中心的回文字符串長度RL[j],對于以p[j]為中心的回文字符串有兩種情況:

  • j為中心的回文字符的最左邊j-(RL[j]-1) 大于等于 maxidR關于maxid的對稱點maxid-(maxidR-maxid),在這種情況下,我們可以推斷出以i為對稱點的RL[i]的值最小為RL[j]
  • 大于的情況,可以保證以i為對稱點的RL[i]至少為(maxidR-i)+1

當然這上面只是推測出的 最小情況,之后仍然要繼續(xù)遍歷來更新RL[i]的值。

(2) i >= maxidR

這時候沒有任何的已知信息,我們只能從i的左右兩邊慢慢遍歷。

實現(xiàn)代碼

class Untitled {
    
    static int maxSynStr(char ip[], int len) {
        int size = 2*len + 1;
        char a[] = new char[size];
        int RL[] = new int[size];
        int i = 0;
        int n;
        while (i < len) {
            a[(i<<1)+1] = ip[i];
            a[(i<<1)+2] = '#';
            i++;
        }
        a[0] = '#';
        //最遠字符的中心對稱點。
        int maxid = 0;
        //探索到的最遠字符。
        int maxidR = 0;
        int ans = 0;
        RL[0] = 1;
        for (i = 1; i < size; i++) {
            //首先推測出i為中心的最小回文半徑。
            int offset = 0;
            if (i < maxidR) {
                //j是關于maxid在左邊的對稱點。
                int j = maxid-(i-maxid);
                //獲取之前計算出的以j為中心的回文半徑。
                if (j-(RL[j]-1) >= maxid-(maxidR-maxid)) {
                    offset = RL[j]-1;
                } else {
                    offset = maxidR-maxid;
                } 
            }
            do {
                offset++;
            } while(i-offset >= 0 && i+offset < size && a[i+offset] == a[i-offset]);
            //最后一次是匹配失敗的,因此要減去1。
            offset--;
            //RL[i]的值包括了自己,因此要加1。
            RL[i] = offset+1;
            //更新當前最大的回文半徑。
            if (i+offset > maxidR){
                maxidR = i+offset;
                maxid = i;
            }
            if (RL[i] > ans) {
                ans = RL[i];
            }
        }
        return ans-1;
    } 
    
    public static void main(String[] args) {
        char[] source = "google".toCharArray();
        System.out.println("result=" + maxSynStr(source, 6));
    }
}

運行結果:

>> result=4

2.3 將字符串中的 * 移到前部,并且不改變非 * 的順序

問題描述

將字符串中的*移到前部,并且不改變非*的順序,例如ab**cd**e*12,處理后為*****abcde12

解決思路

我們可以將整個數(shù)組分為兩個部分:有可能包含*字符的部分和一定不包含*字符的部分。初始時候,整個數(shù)組只有有 有可能包含*字符的部分,那么我們就可以 從后往前 遍歷,每遇到一個非*的字符就把它放到 一定不包含*字符的部分,由于需要保持非*的順序,因此需要將它插入到該部分的首部。

實現(xiàn)代碼

class Untitled {

    static void moveNullCharPos(char p[], int length) {
        if (length > 1) {
            char t;
            char c;
            int lastCharIndex = length;
            //必須要從后向前掃描。
            for(int j = length-1; j >=0 ;j--) {
                if ((c = p[j]) != '*') {
                    lastCharIndex--;
                    t = p[lastCharIndex]; p[lastCharIndex] = p[j]; p[j] = t;
                }
            }
        }
        System.out.println(p);
    }

    public static void main(String[] args) {
        char[] source = "ab**cd**e*12".toCharArray();
        moveNullCharPos(source, source.length);
    }
}

運行結果:

>> *****abcde12

2.4 不開辟用于交換的空間,完成字符串的逆序(C++)

問題描述

不開辟用于交換的空間,完成字符串的逆序。

解決思路

這里利用的是 兩次亦或等于本身 的思想。

實現(xiàn)代碼

#include <iostream>
using namespace std;

void reverWithoutTemp(char *p, int length){
    int i = 0;
    int j = length-1;
    while (i < j) {
        p[i] = p[i]^p[j];  
        //實際上是p[i]^p[j]^p[j],這里的p[i]和p[j]指的是原始數(shù)組中的值。
        p[j] = p[i]^p[j];  
        //實際上是(p[i]^(p[i]^p[j]^p[j]))^(p[i]^p[j]^p[j]),這里的p[i]和p[j]指的是原始數(shù)組中的值。
        p[i] = p[i]^p[j];  
        i++;j--;
    }
    std::cout << p << std::endl;
}

int main() {
    char p[] = "1234566";
    reverWithoutTemp(p, 7);
    return 0;
}

運行結果:

>> 6654321

2.5 最短摘要生成

問題描述

給定一段描述w和一組關鍵字q,我們從這段描述中找出包含所有關鍵字的最短字符序列,這個最短字符序列就稱為 最短摘要

  • 最短字符序列必須包含所有的關鍵字
  • 最短字符序列中關鍵字的順序可以是隨意的

解決思路

假設我們的輸入序列如下所示,其中w表示非關鍵字的字符串,而q則表示關鍵字的字符串:

w0,w1,w2,w3,q0,w4,w5,q1,w6,w7,w8,q0,w9,q1

這里,我們引入額外的三個變量pStartpEndflag數(shù)組,flag數(shù)組用于統(tǒng)計pStartpEnd之間關鍵字的命中情況。

這里說明一下flag數(shù)組的作用,flag數(shù)組和關鍵字p數(shù)組的長度相同,每命中一個關鍵字,就將flag數(shù)組的對應位置+1,而flagSize只有在每次遇到一個新的關鍵字時才更新,因此它表示flag數(shù)組中 不重復的關鍵字的個數(shù)

算法的步驟如下:

  • 第一步:我們將pEndw[0]開始移動,每發(fā)現(xiàn)一個命中的關鍵字,就更新flag[]數(shù)組,直到w[pStart,..,pEnd] 包含了所有的關鍵字,即w0,w1,w2,w3,q0,w4,w5,q1
  • 第二步:開始移動pStart,這時候pStart,..,pEnd之間的長度將會逐漸變短,在移動的過程中,同時更新flag[]數(shù)組,直到pStart,...,pEnd之間 不再包含所有的關鍵字,這時候就可以求得 目前為止的最短摘要長度,即q0,w4,w5,q1
  • 第三步:重復第一步的操作,移動pEnd使得pStart,...,pEnd重新 包含所有的關鍵字,再執(zhí)行第二步的操作來 更新最短摘要長度,直到pEnd遍歷到w的最后一個元素。

實現(xiàn)代碼

class Untitled {

    static int findKey(String[] p1, String p2) {
        int len = p1.length;
        for(int i = 0; i < len; i++) {
            if(p1[i].equals(p2))
                return i;
        }
        return -1;
    }
    
    //p1為原始數(shù)據(jù),p2為所有的關鍵詞。
    static int calMinAbst(String[] p1, String[] p2) {
        int p1Len = p1.length;
        int p2Len = p2.length;
        int r;
        int shortAbs = Integer.MAX_VALUE;
        int tAbs = 0;
        int pBegin = 0;
        int pEnd = 0;
        int absBegin = 0;
        int absEnd = 0;
        int flagSize = 0;
        int flag[] = new int[p2Len];
        //初始化標志位數(shù)組。
        for (int i = 0; i < p2Len; i++) {
            flag[i] = 0;
        }
        while (pEnd < p1Len) {
            //只有先找到全部的關鍵詞才退出循環(huán)。
            while (flagSize != p2Len && pEnd < p1Len) {
                r = findKey(p2, p1[pEnd++]);
                if (r != -1) {
                    if (flag[r] == 0) {
                        flagSize++;
                    }
                    flag[r]++;
                }
            }
            while (flagSize == p2Len) {
                if ((tAbs = pEnd-pBegin) < shortAbs) {
                    shortAbs = tAbs;
                    absBegin = pBegin;
                    absEnd = pEnd-1;
                }
                r = findKey(p2, p1[pBegin++]);
                if (r != -1) {
                    flag[r]--;
                    if (flag[r] == 0) {
                        flagSize--;
                    }
                }
            }
        }
        for (int i = absBegin; i <= absEnd; i++) {
            System.out.print(p1[i] + ",");
        }
        System.out.println("\n最短摘要長度=" + tAbs);
        return shortAbs;
    }

    public static void main(String[] args) {
        String keyword[] = {"微軟", "計算機", "亞洲"};
        String str[] = { 
            "微軟","亞洲","研究院","成立","于","1998","年",",","我們","的","使命",
            "是","使","未來","的","計算機","能夠","看","、","聽","、","學",",",
            "能","用","自然語言","與","人類","進行","交流","。","在","此","基礎","上",
            ",","微軟","亞洲","研究院","還","將","促進","計算機","在","亞太","地區(qū)",
            "的","普及",",","改善","亞太","用戶","的","計算","體驗","。","”"
        };
        calMinAbst(str, keyword);
    }
}

運行結果

>> 微軟,亞洲,研究院,還,將,促進,計算機,
>> 最短摘要長度=7

2.6 最長公共子序列

問題描述

經(jīng)典的LCS問題,這里主要解釋一下最長公共子序列的含義。最長公共子串和最長公共子序列的區(qū)別:子串串的一個連續(xù)的部分子序列則是 不改變序列的順序,而從序列中去掉任意的元素 而獲得的新序列。

解決思路

經(jīng)典的LCS問題,原理可以參考這篇被廣泛轉(zhuǎn)載的文章 程序員編程藝術第十一章:最長公共子序列問題,這里給出簡要介紹一下基本的思想。

LCS基于下面這個定理:

LCS 算法定理

最終目的是構建類似于下面的一個矩陣:

LCS 矩陣
  • 對于矩陣,定義c[i][j]:它表示字符串序列A的前i個字符組成的序列A和字符串序列B的前j個字符組成的序列B之間的最長公共子序列的長度,其中i<=A.len,并且j<=B.len
  • 如果A[i]=B[j],那么AB之間的最長公共子序列的最后一項一定是這個元素,也就是c[i][j] = c[i-1][j-1]+1
  • 如果A[i]!=B[j],則c[i][j]= max(c[i-1][j], c[i][j-1])
  • 初始值為:c[0][j]=c[i][0]=0

代碼實現(xiàn)

class Untitled {

    static void LCS(char a[], int aLen, char b[], int bLen){
        int c[][] = new int[bLen+1][aLen+1];
        for (int i = 1; i < bLen+1; i++) {
            for (int j = 1; j < aLen+1; j++) {
                if (a[j-1] == b[i-1]) {
                    c[i][j] = c[i-1][j-1] + 1;
                } else {
                    c[i][j] = (c[i-1][j]>c[i][j-1]) ? c[i-1][j]:c[i][j-1];
                }
            }
        }
        int csl = c[bLen][aLen];
        char p[] = new char[csl+1];
        int i = bLen, j = aLen;
        while (i > 0 && j > 0 && c[i][j] > 0) {
            if (c[i][j] == c[i-1][j]) {
                i--;
            } else if(c[i][j] == c[i][j-1]) {
                j--;
            } else if(c[i][j] > c[i-1][j-1]) {
                p[c[i][j]] = a[j-1];
                i--;j--;
            }
        }
        for (i = 1; i <= csl; i++) {
            System.out.print(p[i]);
        }
    } 

    public static void main(String[] args) {
        char p1[] = "aadaae".toCharArray();
        char p2[] = "adaaf".toCharArray();
        LCS(p1, p1.length, p2, p2.length);
    }
}

運行結果

>> adaa

更多文章,歡迎訪問我的 Android 知識梳理系列:

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容

  • 最長回文子串——Manacher 算法 1. 問題定義 最長回文字符串問題:給定一個字符串,求它的最長回文子串長度...
    林大鵬閱讀 2,800評論 0 6
  • 這次要記錄的是一個經(jīng)典的字符串的題目,也是一個經(jīng)典的馬拉車算法的實踐。相信在很多地方都會考到或者問到這道題目,這道...
    檸檬烏冬面閱讀 2,939評論 0 9
  • 上一篇KMP算法之后好幾天都沒有更新,今天介紹最長回文子串。 首先介紹一下什么叫回文串,就是正著讀和倒著讀的字符順...
    zero_sr閱讀 2,349評論 2 8
  • 回溯算法 回溯法:也稱為試探法,它并不考慮問題規(guī)模的大小,而是從問題的最明顯的最小規(guī)模開始逐步求解出可能的答案,并...
    fredal閱讀 13,738評論 0 89
  • 最長回文串問題是一個經(jīng)典的算法題。 0. 問題定義 最長回文子串問題:給定一個字符串,求它的最長回文子串長度。如果...
    曾會玩閱讀 4,091評論 2 25