1. 程式人生 > >連結串列、棧、佇列、KMP相關知識點

連結串列、棧、佇列、KMP相關知識點

# 連結串列、棧與佇列、kmp; ## 陣列模擬單鏈表: 用的最多的是鄰接表--就是多個單鏈表: 作用:儲存樹與圖 需要明確相關定義: 為什麼需要使用陣列模擬連結串列 1. 比使用結構體 或者類來說 速度更快 2. 程式碼簡潔 3. 演算法題:空間換時間 [題目詳情](https://www.acwing.com/solution/content/3472/) 圖解: ![](https://gitee.com/xbhog/BlogImg/raw/master/null/%E6%95%B0%E7%BB%84%E6%A8%A1%E6%8B%9F%E5%8D%95%E9%93%BE%E8%A1%A8.png) head儲存連結串列頭,e[]儲存節點的值,ne[]儲存節點的next指標,idx表示當前用到了哪個節點 ```java import java.util.*; public class Main{ static int N = 100010; static int idx,head; static int[] e = new int[N]; static int[] next = new int[N]; // 初始化 static void init(){ head = -1; idx = 0; } // 向連結串列頭插入一個數 static void insertHead(int x){ e[idx] = x; next[idx] = head; head = idx++; } // 向k位置插入x static void insert(int k,int x){ e[idx] = x; next[idx] = next[k]; next[k] = idx++; } // 刪除k位置的數 static void delete(int k){ next[k] = next[next[k]]; } // 主函式 public static void main(String[] args){ Scanner sc = new Scanner(System.in); init(); int n = sc.nextInt(); while(n-- != 0){ String s = sc.next(); if(s.equals("H")){ int x = sc.nextInt(); insertHead(x); }else if(s.equals("D")){ int k = sc.nextInt(); if(k == 0) head=next[head]; else delete(k-1); }else if(s.equals("I")){ int k = sc.nextInt(); int x = sc.nextInt(); insert(k-1,x); } } for(int i =head;i != -1;i=next[i]){ System.out.print(e[i]+" "); } } } ``` ## 陣列模擬雙鏈表: 作用:優化某些問題 [題目詳情](https://www.acwing.com/solution/content/5052/) ## 棧: 先進後出 ```java // tt表示棧頂 int stk[N], tt = 0; // 向棧頂插入一個數 stk[ ++ tt] = x; // 從棧頂彈出一個數 tt -- ; // 棧頂的值 stk[tt]; // 判斷棧是否為空 if (tt > 0) { } ``` ## 佇列: 先進先出 ```java 1. 普通佇列: // hh 表示隊頭,tt表示隊尾 int q[N], hh = 0, tt = -1; // 向隊尾插入一個數 q[ ++ tt] = x; // 從隊頭彈出一個數 hh ++ ; // 隊頭的值 q[hh]; // 判斷佇列是否為空 if (hh <= tt) { } ``` ```java 2. 迴圈佇列 // hh 表示隊頭,tt表示隊尾的後一個位置 int q[N], hh = 0, tt = 0; // 向隊尾插入一個數 q[tt ++ ] = x; if (tt == N) tt = 0; // 從隊頭彈出一個數 hh ++ ; if (hh == N) hh = 0; // 隊頭的值 q[hh]; // 判斷佇列是否為空 if (hh != tt) { } ``` ## 單調棧: [題目詳情](https://www.acwing.com/problem/content/832/) 每個數找到左邊離自己最近且比自己小的數: 暴力演算法 : ```java import java.util.*; import java.io.*; public class Main{ static int N = 100010; static int[] q = new int[N]; public static void main(String[] args){ Scanner sc = new Scanner(System.in); int n = sc.nextInt(); for(int i = 0;i < n; i++){ q[i] = sc.nextInt(); } for(int i = 0;i < n;i++){ int index =0; for(int j = i-1;j >=0;j--){ if(q[i]>q[j]){ index = j; System.out.print(q[j]+" "); break; } } if(q[i]<=q[index]){ System.out.print("-1 "); } } } } ``` AC程式碼: ```java import java.util.*; public class Main{ static int N = 100010; static int[] stk = new int[N]; static int tt=0; public static void main(String[] args){ Scanner sc = new Scanner(System.in); int n = sc.nextInt(); for(int i = 0; i < n; i++){ int x = sc.nextInt(); // 如果棧中還有元素並且棧頂元素大於x的話,從棧頂彈出; while(tt != 0 && stk[tt] >
= x) tt--; // 上面結束以後是棧頂元素小於輸入的元素; stk[tt] < x; 這樣就保證了x元素在左邊並且小於本身的元素是棧頂元素; //如果棧中有元素,輸出棧頂元素 if(tt != 0) System.out.print(stk[tt]+" "); // 否則棧中沒有元素 else System.out.print("-1"+" "); // 將元素加入棧中 stk[++tt] = x; } } } ``` ## 單調佇列: [滑動視窗:](https://www.acwing.com/problem/content/156/) 解題思路: 1. 判斷隊頭出沒出視窗 if true-->hh++; 2. 求最小值,保持佇列單調上升,判斷隊尾元素tt與當前元素a[i]的大小,若tt >=a[i],剔除隊尾元素; 3. 求最大值,保持佇列單調下降,判斷隊尾元素tt與當前元素a[i]的大小,若tt <=a[i],剔除隊尾元素; 4. 將當前元素下標加入隊尾; 5. 如果滿足條件輸出 注:佇列是先進先出模式,只有在佇列中保持單調性 才能保證,隊頭為最小值或者最大值; ```java import java.io.*; public class Main{ static int N = 1000010; // 原陣列 static int[] a = new int[N]; // 佇列 static int[] q = new int[N]; static int hh=0,tt=-1; public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out)); String[] num = br.readLine().split(" "); // a陣列中由n個元素 int n = Integer.parseInt(num[0]); // 滑動視窗的大小 int k = Integer.parseInt(num[1]); String[] nums = br.readLine().split(" "); // 初始化a陣列 for(int i = 0; i < n; i++) a[i] = Integer.parseInt(nums[i]); // 查詢最小值 for(int i = 0; i < n; i++){ // 判斷當前視窗是否大於滑動視窗的大小 if(hh <= tt && i-q[hh]+1 > k) hh++; // 判斷如果有元素並且隊尾元素大於入隊元素,則捨棄隊尾元素來保證單調上升的佇列; while(hh <= tt && a[q[tt]] >= a[i]) tt--; //向佇列加入元素下標 q[++tt] = i; // 如果下標大於等於視窗大小,那麼輸出隊頭元素 if(i+1 >=k) bw.write(a[q[hh]]+" "); } bw.write("\n"); hh = 0; tt = -1; // 查詢最大值 for(int i = 0; i < n; i++){ // 判斷當前視窗是否大於滑動視窗的大小 if(hh <= tt && i-q[hh]+1 > k) hh++; // 判斷如果有元素並且隊尾元素小於入隊元素,則捨棄隊尾元素來保證單調上升的佇列; while(hh <= tt && a[q[tt]] <= a[i]) tt--; //向佇列加入元素下標 q[++tt] = i; // 如果下標大於等於視窗大小,那麼輸出隊頭元素 if(i+1 >
=k) bw.write(a[q[hh]]+" "); } bw.flush(); br.close(); bw.close(); } } ``` ## KMP演算法: KMP定義:是取自三個發明人的首字母組成的; 作用:是一個字串匹配演算法,對暴力的一種優化 kmp實現的方式:求next[]、以及匹配字串 明確其中的概念: 對字串的匹配,需要兩個字串:長字串為模板字串,短的字串為匹配字串; **字首與字尾的概念:(很重要)** 舉個例子:列舉 ```markdown ababa ``` 其字首為: ```markdown a,ab,aba,abab ``` 其後綴: ```markdown baba,aba,ba,a ``` 其中字首與字尾的最長且相同的元素字串是:`aba` ;長度length:3。 模板串的匹配: ![](https://gitee.com/xbhog/BlogImg/raw/master/null/kmp%E5%8C%B9%E9%85%8D.gif) ![image-20210220180508330](https://gitee.com/xbhog/BlogImg/raw/master/null/kmp%E5%8C%B9%E9%85%8D%E8%BF%87%E7%A8%8B.png) 匹配失敗的話,假設p1串往後移動到p2串位置,表明1串=2.2串; $$ \because 匹配失敗使得p1移動到p2位置\\ \therefore s-1 = p2-2.2\\ \because p2是p1的平移\\ \therefore p1 = p2\\ \therefore p1-2.1 = p2-2.2\\ 又\because s = p1\\ \therefore s-1 = p1-3\\ 由上可知:\\ p2-2.2 = p1-3;\\ p1-2.1 = p1-3; $$ 如果匹配失敗,p串最少往右移動多少(看next[]陣列),可以使得p串與s串相等,由圖可知:往後移動多少是看p串,如果我們能預處理來這個東西:使得p1-2.1串能與p1.3串相等;這個相等的最大值是多少,值越大,則表示往後移動p串的距離越少; ```java // kmp匹配過程,遍歷s模板每個元素 for(int i =1, j =0; i<=m; i++ ){ // 如果j回退到0或者i位置元素與j+1位置的元素不相同,那麼執行回退操作, //j退回next[i]處,即字首與字尾相同的區間最後元素位置 while(j != 0 && s[i] != p[j+1]) j = next[j]; if(s[i] == p[j+1]) j++; // 如果匹配成功 if(j ==n){ // 輸出匹配元素在s模板中的起始位置 bw.write(i-n+" "); // 繼續匹配; j = next[j]; } ``` 也就是p串的字首與字尾相同的最大字串是多少;----也就是next[]陣列; **next[]陣列(難點)** 其中next[j]陣列表示的是:子串p[1~j]的最長相等前後綴的字首最後一位的下標。 對 p = “abcab” p a b c a b 下標 1 2 3 4 5 next[ ] 0 0 0 1 2 對next[ 1 ] :字首 = 空集—————字尾 = 空集—————next[ 1 ] = 0; **(特殊點)** 對next[ 2 ] :字首 = { a }—————字尾 = { b }—————next[ 2 ] = 0; 對next[ 3 ] :字首 = { a , ab }—————字尾 = { c , bc}—————next[ 3 ] = 0; 對next[ 4 ] :字首 = { a , ab , abc }—————字尾 = { a . ca , bca }—————next[ 4 ] = 1; 對next[ 5 ] :字首 = { a , ab , abc , abca }————字尾 = { a , ab , cab , bcab}————next[ 5 ] = 2; ![image-20210221103501736](https://gitee.com/xbhog/BlogImg/raw/master/null/next%E6%95%B0%E7%BB%84.png) ```java // 實現next陣列(找到字首與字尾相同的最大元素長度) int[] next = new int[n+1]; //next[1] = 0; for(int i = 2,j =0; i<= n;i++){ // 如果j沒有回退到0並且i位置元素與j+1位置的元素不相同,那麼執行回退操作, //j退回next[i]處,即字首與字尾相同的區間最後元素位置 while(j !=0 && p[i] != p[j+1]) j = next[j]; // 如果i與j+1相同,那麼移動j向後匹配 if(p[i] == p[j+1]) j++; // p[1,j] = p[i-j+1,i];字首與字尾相同;i表示終點 next[i] = j; } ``` **完整程式碼:** ```java import java.util.*; import java.io.*; //下標為什麼從1開始,簡化程式碼的複雜度; public class Main{ public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out)); // 對模式串p進行操作 int n = Integer.parseInt(br.readLine()); char[] p = new char[n+1]; // 將輸入的字元儲存到緩衝區 String pstr = br.readLine(); for(int i = 1; i <=n;i++){ // 取出的字串下標-1; p[i] = pstr.charAt(i-1); } // 對模板串進行操作 int m = Integer.parseInt(br.readLine()); char[] s = new char[m+1]; String sstr = br.readLine(); for(int i = 1;i <= m;i++){ s[i] = sstr.charAt(i-1); } // 實現next陣列 int[] next = new int[n+1]; //next[1] = 0; for(int i = 2,j =0; i<= n;i++){ // 如果j回退到0或者i位置元素與j+1位置的元素不相同,那麼執行回退操作, //j退回next[i]處,即字首與字尾相同的區間最後元素位置 while(j !=0 && p[i] != p[j+1]) j = next[j]; // 如果i與j+1相同,那麼移動j向後匹配 if(p[i] == p[j+1]) j++; // p[1,j] = p[i-j+1,i];字首與字尾相同;i表示終點 next[i] = j; } // kmp匹配過程,遍歷s模板每個元素 for(int i =1, j =0; i<=m; i++ ){ // 如果j回退到0或者i位置元素與j+1位置的元素不相同,那麼執行回退操作, //j退回next[i]處,即字首與字尾相同的區間最後元素位置 while(j != 0 && s[i] != p[j+1]) j = next[j]; if(s[i] == p[j+1]) j++; // 如果匹配成功 if(j ==n){ // 輸出匹配元素在s模板中的起始位置 bw.write(i-n+" "); // 繼續匹配; j = next[j]; } } bw.flush(); br.close(); bw.close(); } } ``` ## 結束: 感謝大家看到最後,如果有錯誤,歡迎指正! 參考文獻:https://www.acwing.com/solution/conten