數據結構與算法 —— 04 串

2017/06/21

1.串(String)

㈠'邏輯結構':
串是由零個或多個字符組成的有限序列,又名字符串
s = "a1a2...an"; (n>=0)

注意:串中字符的位置是從1開始的,

空串:零個字符的串,即值為:null, 其長度為 0
空格串:是只包含空格的串,是有內容長度的。這與空串不同,值為:""(長度為0), " "(長度為1), " "(長度為3),等

(1)串的比較

通過組成串的字符之間的編碼(即字符在對應字符集中的序號)來進行的。

計算機常用的字符集:
ASCII:早前用7位二進制數表示,總共表示128個字符, 后來擴展為8位二進制表示,總共表示256個字符(英語語言基本滿足,其他語言可能不夠)

Unicode:采用16位二進制表示,總共可以表示 2^16 個字符,65536個字符。
注意:為了兼容ASCII,其前256個字符和ASCII完全相同。

(2)串的抽象數據類型

串的邏輯結構和線性表很像,只是串的元素針對的是字符集,它們是一個個字符而已。
況且串更多的是看成一個整體的,如:"123",這是整體

線性表則更關注的是對單個元素的操作,比如查找一個元素,插入一個元素等
串中更多的是針對子串,比如查找 子串的位置,得到某個位置的子串等

基本操作:

① 串復制:將某個串復制給當前串
② 判空:若當前串為空,則返回true,否則返回false
③ 串比較:
④ 求串長:返回當前串的字符個數
⑤ 串連接:將s1和s2連接成一個新串,賦值給串T(即生成新的串)
⑥ 求子串:返回當前串中的第i個字符開始的長度為k的子串
⑦ 子串定位:返回子串在主串中首次出現的位置
⑧ 串替換:用子串x替換當前串中的子串y
⑨ 插入子串:將子串插入到當前串中的某個位置
⑩ 刪除子串:從當前串中刪除指定的子串
⑾ 大寫轉小寫:
⑿ 小寫轉大寫:
⒀ 串壓縮:刪除串中首尾處的空格
㈡'物理存儲結構'

(1)串的順序存儲
與線性表一樣,串的順序存儲結構是采用一組地址連續的存儲單元來存儲串中的字符序列,一般用數組來實現的

注意:這里采用的數組來實現串的順序存儲時,要注意可能是采用定長數組。這會在進行串連接、新串插入、替換等造成上溢截斷,從而使數據不完整。因此,為了解決這個問題,使數組的長度在程序執行過程中被動態分配。

典型的例子,如計算機中的"堆",是自由存儲區,可由C語言的動態分配函數malloc(), free() 來管理。

表示串的長度有三種常見的方式:

  1. 用一個變量來表示串的長度
  2. 在串尾存儲一個不會在串中出現的特殊字符作為串的終結符。例如'\0'
  3. 用數組的0號單元存放串的長度,串值從1號單元開始存放

'代碼實現'

public class MyString {
    private int maxSize = 10; //串中字符數組的初始長度
    private char[] chars; //存儲字符的數組
    private int lenght; //記錄串的長度的變量,即上面講的第一種方式
    //1.初始化串: 采用默認長度
    public MyString() {
        this.chars = new String[maxSize];
        this.length = 0;
    }

    public MyString(int n) {
        this.maxSize = n;
        this.chars = new String[maxSize];
        this.length = 0;
    }

    //2.將串t復制給當前串,即調用該方法的字符串
    public void copy(MyString t) {
    
    }
    //3.判空
    public boolean isEmpty() {
        return length == 0;
    }

    //4.串的比較
    public int compare(MyString t) {
    
    }

    //5.求串的長度
    public int getLength() {
        return length;
    }

    //6. 清空當前串
    public boolean clear() {}

    //7.將指定串t連接到當前串的尾部
    public void concat(MyString t) {}

    //8.獲得當前串的子串
    public MyString subString(int pos, int len) {}

    //
    public MyString subString(int pos) {}

    //9.返回子串t在當前串中首次出現的位置
    public int index(Mystring t) {}

    //10.返回子串t在當前串中最后一次出現的位置
    public int lastIndext(MyString t) {}

    //11.替換,用v代替所有與t相同的子串
    public int replace(MyString t, MyString v) {}

    //12.插入子串
    public boolean insert(MyString s, int pos) {}

    //13.刪除子串
    public boolean delete(int pos, int len) {}

    //刪除
    public int remove(MyString s) {}
    //14.轉換為大寫
    public void toUpperCase() {}

    //15.轉換為小寫
    public void toLowerCase() {}
}

(2)串的鏈式存儲結構
與線性表的鏈式結構表示相似,但由于串結構的每個元素數據是一個字符,如果此時采用在鏈表的每個結點處存儲一個串的元素,就會造成大量的浪費(因為,每個結點的數據域對多個存儲單元)

        ┌──┬──┬──┬──┬─┐    ┌──┬──┬──┬──┬─┐    ┌──┬──┬──┬──┬─┐
    ——> │ A│ B│ C│ D│-│——> │ E│ F│ G│ H│-│——> │I │J │ #│ #│^│  
        └──┴──┴──┴──┴─┘    └──┴──┴──┴──┴─┘    └──┴──┴──┴──┴─┘

因此,可以考慮在一個結點處存放多個字符(如上圖),最后一個結點若是未被占滿,就用#,^等非串值字符補全

注意:串的鏈式存儲結構在連接串與串的操作中有一定方便外,其他的一些操作靈活性不如順序存儲結構

㈢ '模式匹配'

在當前串中尋找某個子串的過程稱為模式匹配,其中該子串稱為模式串。如果成功,則返回子串在當前串中的首次出現的存儲位置(或序號),否則匹配失敗。

模式匹配的算法:
1)樸素的模式匹配算法(簡單的意思)
基本思想:

第 1 趟:首先將子串的第1個字符和主串的第1個字符比較,若相等,再比
較子串的第2個字符和主串的第2個字符的,
    若相等再比較下一個, ....
    若不相等,則進行下一趟的比較。
        第 2 趟:將子串的第1個字符和主串的第2個字符比較....
        ...
        第 i 趟:將子串的第1個字符和主串的第 i 個字符比較...
        直到匹配成功或失敗。

給該類算法取個名稱:Brute(野蠻,粗魯) Force, BF

此類算法低效,最壞情況:O((n-m+1)*m)
平均情況:O(n+m)

'代碼實現'

/**
* @param s 主串
* @param t 子串
* @param pos 起始位置
* @return 返回子串在主串中首次出現的位置
* 注意:實際上字符的位置是從1開始,這里為了使用toCharArray()方便,將字符串轉換為
*       字符數組,所以,字符的位置便從0開始的
*/
public static int index(String s, String t, int pos) {
    //數據驗證
    if(pos < 0) {
        System.out.println("輸入的pos參數不合理,應該是大于等于0的數");
        return -1;
    } else if(pos >= (s.length() - t.length() + 1)){
        System.out.println("輸入的pos參數不合理");
        return -1;
    }
    char[] chs = s.toCharArray();
    char[] cht = t.toCharArray();
    //完成正常的匹配任務
    int i = pos; //控制大循環,即針對主串的
    int j = 0; //針對子串的
    while (i < (s.length() - t.length() + 1) && j < t.length()) {
        if (chs[i] == cht[j]) {
            //部分匹配成功
            i ++;
            j ++;
        }else {
            //匹配不成功,切換到主串的的下一個地方
            //核心部分,仔細體會
            i = i - j + 1; //返回到主串的這次起始位置的下一個位置
            j = 0; //返回子串的起始位置(即開頭處)
        }
    }

    if (j == t.length()) {
        return i-t.length() + 1; //返回子串的位置
    }else {
        return -1;
    }
}

為了避免重復遍歷,研究除了下面的算法:KMP算法,克努特-莫里斯-普拉特算法

2) KMP算法

核心思想:就是避免不必要的回溯(即重復遍歷),那么什么是不必要的回溯,主要是由模式串來決定的

next數組:
前綴是固定的,后綴是相對的

'代碼描述':

/**
 * KMP算法:核心是求取next數組,其大體框架同樸素算法
 * 如果將數組的第一個存儲單元拿來存儲數組長度,則其變成就會簡單一些,不過本質
 * 都是一樣的
 * 注意:KMP的優化部分
 * @author Administrator
 */
public class KMP {

    /**
     * 返回子串t在主串s中從pos之后首次出現的位置
     * 
     * @param s
     * @param t
     * @param pos
     * @return
     */
    public int kmpIndex(String s, String t, int pos) {
        
        // 數據驗證
        if (pos < 0) {
            System.out.println("輸入的pos參數不合理,應該是大于等于0的數");
            return -1;
        } else if (pos >= (s.length() - t.length() + 1)) {
            System.out.println("輸入的pos參數不合理");
            return -1;
        }
        
        char[] chrs = s.toCharArray();
        char[] chrt = t.toCharArray();
        int i = pos; //控制主串,從 pos 之后開始檢索
        int j = 0; //控制子串,從子串的頭部開始檢索
        //獲取next數組
        int[] next = getNext(t);
        while(i < chrs.length && j < chrt.length) {
            //注意:相對于樸素算法,這里要注意j的調整,有可能j變為-1,說明j指
            // 到子串的外面去了。所以,當 j = -1 時需要修正
            if(-1 == j || chrs[i] == chrt[j]) {
                i++;
                j++;
            } else {
                //i = i - j + 1; //在這里i就不用回溯了
                j = next[j] - 1;                
            }           
        }
        
        if(j == chrt.length) {  
            return i - chrt.length + 1;     
        }else {         
            return -1;
        }       
    }

    /**
     * 計算next數組
     * 字符的前后綴,如果有1個字符相等,則next數組對應的是2,有2個字符相等,就對應是3,即如果有k個字符相等,則對應的數組是(k+1),
     * @param t 為模式字符串
     */
    private int[] getNext(String t) {
        int[] next = new int[t.length()];

        char[] chrs = t.toCharArray(); // 轉換為數組,方便進一步操作
        next[0] = 0;
        // next[1] = 1;
        int i = 0; // 控制后綴,只前進,不后退
        int j = -1; // 控制前綴,自動調節后退的位置,等于-1時,是個特殊位置,要注意
        while (i < t.length() - 1) {

            if (j == -1 || chrs[i] == chrs[j]) {
                i++;
                j++;
                /**
                 * 對next數組的優化部分: 
                 * 這里出現了缺陷,如:aaaaax,還是會造成一些不必要的匹配操作,
                 * 因此。要對next數組的進行調整
                 */
                if(chrs[i] == chrs[j]) {
                    next[i] = next[j];      
                }else {
                    next[i] = j + 1;
                }
                
            } else {
                // 不相等時,j得退回到上一次相等的地方
                j = next[j] - 1;
            }
        }
        return next;
    }

    // 打印數組
    public void getPrint(int[] next) {
        System.out.print("[");
        for (int i = 0; i < next.length; i++) {
            if (i == next.length - 1) {
                System.out.println(next[i] + "]");
            } else {
                System.out.print(next[i] + ", ");
            }
        }
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容