【算法編程】KMP、Manacher和BFPRT算法

一、KMP算法

1、算法背景

  KMP 算法原本是用來解決包含問題的,具體問題如下:

  • 給定一個主串 str1 和模式串 str2 ,要求找出 str2str1 中出現的位置,此即串的模式匹配問題。

例如:

  • str1:aaaaaab
  • str2aaab

暴力解決方法:

  str1 從0的位置依次往下匹配 str2

在這裏插入圖片描述

  • KMP算法的核心是利用匹配失敗後的信息,儘量減少模式串與主串的匹配次數以達到快速匹配的目的
  • 具體實現就是通過一個 next() 函數實現,函數本身包含了模式串的局部匹配信息

2、預備概念

  在介紹KMP之前我們先來了解一個概念,沒有這個概念後面無法進行。在一個字符串中的一個字符之前的子串的最長前綴和後綴匹配的長度。

例如:

  • str:abcabcd
  • 求字符 d 之前的最長前綴和後綴匹配的長度

前綴不能包含除字符d之外的最後一個字符,後綴不能包含第一個字符

在這裏插入圖片描述

KMP 就是通過一個 next() 函數實現 str2 中每個字符對應的最長前綴和後綴匹配的長度

在這裏插入圖片描述
那麼如何求next數組呢?

在這裏插入圖片描述

public static int[] getNext(char[] str2) {
	if (str2.length == 1) {  //如果str2長度爲1,返回-1
		return new int[] { -1 };
	}
	int[] next = new int[str2.length];
	next[0] = -1;  //位置0
	next[1] = 0;  //位置1
	int i = 2;  //當前來到的位置
	int cn = 0;  //前綴子串後一個字符
	while (i < next.length) {
		if (str2[i - 1] == str2[cn]) {  //如果當前字符的前一個字符和前綴子串後一個字符相等
			next[i++] = ++cn;  //當前數組值+1,當前位置來到下一個位置,
		} else if (cn > 0) {  //cn就是圖中X,Y,Z...位置
			cn = next[cn];
		} else {
			next[i++] = 0;  //否則值爲0
		}
	}
	return next;
}

3、KMP算法

  那麼 KMP 是如何利用上述概念來加速的呢?
在這裏插入圖片描述

4、Java代碼

package day07;

public class Code01_KMP {
	public static int kmp(String str1, String str2) {
		if(str1 == null || str2 == null || str2.length() < 1 || str1.length() < str2.length()){
			return -1;
		}
		char[] s = str1.toCharArray();
		char[] m = str2.toCharArray();
		int si = 0;  //str1當前位置
		int mi = 0;  //str2當前位置
		int[] next = getNext(m);  //獲取str2的最長前綴和後綴匹配的長度
		while(si < s.length && mi < m.length) {
			if(s[si] == m[mi]) {  //如果相等,str1和str2同時跳下一個字符
				si++;
				mi++;
			/**
			 * 不相等,str2往後推,str1中的j對應str2中的0位置,並從X和Z開始匹配
			 * 如果X和Z一直不匹配,str2就往後推,一直將0推到Z處都不相等,此時next[mi]爲-1
			 * 說明一直到第一個字符都不匹配,那麼此時str1往跳下一個字符再開始匹配
			 */
			}else if(next[mi] == -1) {  //str2推到0位置了
				si++;
			}else {  //str2沒推到0,就更新mi位置,也就是Z的位置
				mi = next[mi];
			}
		}
		//如果mi到str2最後了,說明存在匹配的,返回si-mi即匹配子串的開始下標,否則返回-1
		return mi == m.length ? si - mi : -1;  
	}

	public static int[] getNext(char[] str2) {
		if (str2.length == 1) {  //如果str2長度爲1,返回-1
			return new int[] { -1 };
		}
		int[] next = new int[str2.length];
		next[0] = -1;  //位置0
		next[1] = 0;  //位置1
		int i = 2;  //當前來到的位置
		int cn = 0;  //前綴子串後一個字符
		while (i < next.length) {
			if (str2[i - 1] == str2[cn]) {  //如果當前字符的前一個字符和前綴子串後一個字符相等
				next[i++] = ++cn;  //當前數組值+1,當前位置來到下一個位置,
			} else if (cn > 0) {  //cn就是圖中X,Y,Z...位置
				cn = next[cn];
			} else {
				next[i++] = 0;  //否則值爲0
			}
		}
		return next;
	}

	public static void main(String[] args) {
		String str = "abcabcababaccc";
		String match = "ababa";
		System.out.println(kmp(str, match));
	}
}

5、KMP算法應用(1)

  在給定的原字符串 str 後添加字符使得到的新字符串包含兩個原字符串,要求添加的字符是最少的

例如:

  • strabcabc
  • 添加abc,得到新的字符串abcabcabc且裏面包含兩個原字符串,abcabc

當然 abcabcabcabc滿足包含兩個原字符串,但是添加的字符不是最少的

在這裏插入圖片描述

abcabc,最後一個字符後的位置的最長前綴和後綴匹配長度爲3,那麼我們只需要截取下標3到最後位置的字符即是答案,abc組成新的字符串 abcabcabc

Java代碼:

package day07;

public class Code02_KMP_ShortestHaveTwice {
	public static String shortestHaveTwice(String str) {
		if(str == null || str.length() == 0){
			return "";
		}
		char[] charStr = str.toCharArray();
		if(charStr.length == 1) {  //長度爲1,重複即可
			return str + str;
		}
		if(charStr.length == 2) {  //長度爲2,如果前兩個字符相等就加一個字符,如果不等,就重複
			return charStr[0] == charStr[1] ? (str + String.valueOf(charStr[0])) : (str + str);
		}
		int endNext = endNextLength(charStr);  //計算next數組,多了一位
		return str + str.substring(endNext);  //原字符串加截取字符串
	}
	
	public static int endNextLength(char[] charStr) {
		int next[] = new int[charStr.length + 1];
		next[0] = -1;  //第一個字符的next數組值
		next[1] = 0;  //第二個字符的next數組值
		int pos = 2;  //當前位置
		int cn = 0;   //前綴後一個字符
		while(pos < next.length) {
			if(charStr[pos - 1] == charStr[cn]) {
				next[pos++] = ++cn;
			}else if(cn > 0) {
				cn = next[cn]; 
			}else {
				next[pos++] = 0;
			}
		}
		return next[next.length - 1];
	}
	
	public static void main(String[] args) {
		String test1 = "a";
		System.out.println(shortestHaveTwice(test1));

		String test2 = "aa";
		System.out.println(shortestHaveTwice(test2));

		String test3 = "ab";
		System.out.println(shortestHaveTwice(test3));

		String test4 = "abcdabcd";
		System.out.println(shortestHaveTwice(test4));

		String test5 = "abracadabra";
		System.out.println(shortestHaveTwice(test5));

	}
}

5、KMP算法應用(2)

  給定兩個樹 T1T2,求 T1 中是否有一棵子樹和 T2 一樣,一樣返回 true ,否則返回 false

例如:

在這裏插入圖片描述

解題思路:

  • 將兩個數都進行前序序列化,如上圖中兩課樹
  • T1->S1:1_1_1_#_#_1_#_#_1_1_#_#_#_
  • T2->S2:1_1_#_#_#_
  • 我們只需要判斷 T1 中是否包含T2 子串即可,包含返回 true ,否則返回 false

Java代碼:

package day07;

public class Code03_KMP_T1SubtreeEqualsT2 {
	public static class Node{
		public int value;
		public Node left;
		public Node right;
		
		public Node(int data) {
			this.value = data;
		}
	}
	
	//主函數
	public static boolean isSubtree(Node t1, Node t2) {
		String t1Str = serialByPre(t1);  //序列化
		String t2Str = serialByPre(t2);  //序列化
		return getIndexOf(t1Str, t2Str) != -1; 
	}
	
	//前序序列化
	public static String serialByPre(Node head) {
		if(head == null) {
			return "#!";
		}
		String res = head.value + "!";
		res += serialByPre(head.left);
		res += serialByPre(head.right);
		return res;
	}
	
	//KMP,查找子串開始位置
	public static int getIndexOf(String s, String m) {
		if(s == null || m == null || m.length() < 1 || s.length() < m.length()) {
			return -1;
		}
		char[] ss = s.toCharArray();
		char[] ms = m.toCharArray();
		int[] nextArr = getNextArray(ms);
		int index = 0;
		int mi = 0;
		while(index < ss.length && mi < ms.length){
			if(ss[index] == ms[mi]) {
				index++;
				mi++;
			}else if(nextArr[mi] == -1) {
				index++;
			}else {
				mi = nextArr[mi];
			}
		}
		return mi == ms.length ? index - mi : -1;
	}
	
	//獲取next數組
	public static int[] getNextArray(char[] ms) {
		if (ms.length == 1) {
			return new int[] { -1 };
		}
		int[] nextArr = new int[ms.length];
		nextArr[0] = -1;
		nextArr[1] = 0;
		int pos = 2;
		int cn = 0;
		while (pos < nextArr.length) {
			if (ms[pos - 1] == ms[cn]) {
				nextArr[pos++] = ++cn;
			} else if (cn > 0) {
				cn = nextArr[cn];
			} else {
				nextArr[pos++] = 0;
			}
		}
		return nextArr;
	}

	public static void main(String[] args) {
		Node t1 = new Node(1);
		t1.left = new Node(2);
		t1.right = new Node(3);
		t1.left.left = new Node(4);
		t1.left.right = new Node(5);
		t1.right.left = new Node(6);
		t1.right.right = new Node(7);
		t1.left.left.right = new Node(8);
		t1.left.right.left = new Node(9);

		Node t2 = new Node(2);
		t2.left = new Node(4);
		t2.left.right = new Node(8);
		t2.right = new Node(5);
		t2.right.left = new Node(9);

		System.out.println(isSubtree(t1, t2));
	}
}

二、Manacher算法

Manacher算法,又叫“馬拉車”算法,可以在時間複雜度爲O(n)的情況下求解一個字符串的最長迴文子串長度的問題。

1、中心擴展法求解最長迴文子串

  中心擴展法的思想是,遍歷到數組的某一個元素時,以這個元素爲中心,向兩邊進行擴展,如果兩邊的元素相同則繼續擴展,否則停止擴展。算法複雜度爲O(N2)O(N^2)

如下圖:當遍歷到3時

在這裏插入圖片描述

但是單個字符擴展存在缺陷,當字符串長度爲偶數時,例如:1221

1,2,2,1是一個迴文串,然而找不到對稱中心,這樣以一個元素爲中心向兩邊擴展就不好用了

  • 1、分別以單個字符和相鄰兩個字符爲中心擴展(下面代碼使用的是此方法)
  • 2、對1,2,2,1進行填充,比如說用#進行填充得到:#,1,#,2,#,2,#,1,#

Java代碼:

class Solution {
    public String longestPalindrome(String s) {
        
        if(s == null || s.length() < 1)
            return "";
        int start = 0;
        int end = 0;
        //中心擴展法,依次遍歷中心點
        for(int i = 0; i < s.length(); i++){
            //求擴展中心的長度
            int len1 = expandLen(s, i, i);  //以每個字符爲中心
            int len2 = expandLen(s, i, i+1);  //以每相鄰兩字符作爲中心
            int len = Math.max(len1, len2);
            if(len > end - start){
                start = i - (len - 1) / 2;
                end = i + len / 2;
            }
        }
        return s.substring(start, end+1);
        
    }
    
    public int expandLen(String s, int L, int R){
        int left = L;
        int right = R;
        while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
            left--;
            right++;
        }
        return right - left - 1;
    }
}

在這裏插入圖片描述

2、Manacher算法

直接通過例子來說明:

在這裏插入圖片描述

Manacher算法的核心思想,就是利用前面遍歷的時候產生的迴文子串

原理
在這裏插入圖片描述

如上圖:

  • idxidx 表示爲藍色迴文子串的對稱軸(已知)
  • 現在求以 curcur 爲對稱軸的迴文子串(未知)
  • prepre 爲以 idxidx 爲對稱軸,curcur 的對稱位置(遍歷到curcurprepre就已知了)

情況一:ii&#x27; 的迴文子串超出 idxidx 的迴文子串的左邊界

在這裏插入圖片描述

已知 idxidx 爲藍色塊子串的中心軸,現在求以i爲中心軸的迴文子串

  • ii 處於以 idxidx 爲中心軸的迴文子串中,ii&#x27;ii 關於 idxidx 的對稱點,且以 ii&#x27; 爲中心軸的迴文子串已知(橘黃色塊)
  • 其中 ii 指向 ccii&#x27; 指向 bbidxidx 指向 ee(小寫字符爲變量,大寫字母爲具體字符)
  • 那麼由 ee 爲中心的迴文子串中得知,b=cb=c
  • 又因爲 idxidx 的迴文不包括 aadd,所以 a!=da !=da=da=d 時,以 idxidx 的迴文子串還要擴展下去)
  • 又因爲 idxidx 左到 bbidxidx右到cc 相等的,且 a!=da!=d,所以以 cc 爲中心軸的迴文半徑只有idxlocation(c)idx右-location(c)
  • aa 關於以 bb 爲中心的迴文的對稱點爲 aa&#x27;aa&#x27;ee 爲中心軸的迴文的對稱點爲 aa&#x27;&#x27;,那麼 a=a=a!=da=a&#x27;=a&#x27;&#x27;!=d

舉例說明:

由於存在字符串長度爲偶數和奇數,我們使用#填充,如下:
在這裏插入圖片描述

  • 當遍歷到13號B時,以9號D爲中心軸的迴文子串從2號到16號(由於前面已經遍歷過,已知),長度爲 162+1=1516-2+1=15
  • 以5號B爲中心軸的迴文子串從0號到10號(已知),長度爲 100+1=1110-0+1=11
  • 13號B關於9號D的對稱點爲5號B,現在要求以13號B爲對稱軸的迴文子串


1號D和17號E不相等,現在只要盤判定以13號B爲中心軸的迴文子串是否包含17號

  • 如果包括17號E,那麼它關於13號B對稱的點就是9號D,而9號D關於5號B的對稱點就是1號D
  • 根據對稱性可知,17號E應該等於9號D等於1號D,很顯然不相等


所以以13號B爲中心軸的迴文子串不包括17號E,又根據以5號B和9號D爲中心軸的迴文子串可知:

  • 2號#到5號B等於8號#到5號B
  • 10號#到13號B等於16號#到13號B

情況二:ii&#x27; 的迴文子串 idxidx 的迴文子串包含

在這裏插入圖片描述

已知 ii 關於 idxidx 爲中心軸的對稱點 ii&#x27; 的最大回文子串如上圖

  • 因爲 ii&#x27; 的迴文子串不包括 a,ba,ba!=ba!=b
  • 又因爲 a,da,db,cb,c 分別關於 idxidx對稱,記 b=c,a=db=c,a=d ,所以 c!=dc!=d
  • 又因爲在 ccdd 之間是迴文,原因在於 ccdd 之間的字符關於 idxidxaabb 之間對稱,且 aabb 之間是迴文串,所以,ccdd 之間也是迴文串。所以 ii 的迴文子串的長度和 ii&#x27; 相同

舉例說明:
在這裏插入圖片描述

情況三:ii&#x27; 的迴文子串的左邊界與 idxidx 的迴文子串的左邊界重合

在這裏插入圖片描述

idxidx 爲中心軸的迴文子串可知,b=c,a!=db=c,a!=d,且 ii&#x27; 的迴文長度在 aabb 之間(不包括a,ba,b

那麼 ii 的迴文子串的長度至少如上圖所示

  • c=dc=d 時,關於 ii 爲中心軸的迴文還是可以擴展的
  • c!=dc!=d 則剛好是上圖所示的。

舉例說明(c=d時):

在這裏插入圖片描述

ii 關於 idxidx 的對稱點 ii&#x27; 的最長迴文子串如上圖,且 ii&#x27; 的迴文左邊界與 idxidx 重合,所以ii 爲中心的迴文需要從藍色框邊界開始在往左右兩邊試着擴展

情況四:ii&#x27; 的迴文子串沒有被 idxidx 的迴文子串包含

在這裏插入圖片描述

此時,我們沒有任何信息可以利用,只能以 ii 爲中心軸,向左右兩邊擴展。找出它的最長迴文子串。

Java代碼:

package day07;

public class Code04_Manacher {
	//求最長迴文子串
	public static String longestPalindrome(String s) {
        int n = s.length();
        if (n <= 1) return s ;
        
        StringBuilder strb = new StringBuilder();
        strb.append("#");
        for (int i = 0; i < s.length(); i++) {
            strb.append(s.charAt(i));
            strb.append("#");
        }
        
        int len = strb.length();
        int[] radius = new int[len];
        int idx = 0; //表示上一次迴文子串的中心軸下標
        int rad = 1; //idx能夠包含最大的範圍的下一個字符下標
        int j = 0;
        int maxIdx = 0;
        for (int i = 1; i < len; i++) {
            //情況四
            if(i >= rad){
                int count = 1;
                while((i - count) >=0 
                      && (i + count) < strb.length()
                      && strb.charAt(i - count) == strb.charAt(i + count)){
                    count++;
                }
                radius[i] = count - 1;
                if((i + radius[i]) >= rad){
                    idx = i;
                    rad = i + count;
                }
                maxIdx = (radius[i] > radius[maxIdx] ? i : maxIdx);
            }else if(i < rad){
                j = 2*idx - i; //i關於idx的對稱點j
                int idx_radius = idx - radius[idx]; //idx迴文子串的左邊界下標
                int j_radius = j - radius[j];//j的迴文子串的左邊界下標
                if(j_radius > idx_radius){ //情況二
                    radius[i] = radius[j];  //i的迴文子串和其關於idx對稱點的迴文子串長度一樣
                }else if(j_radius < idx_radius){//情況一
                    radius[i] = idx + radius[idx] - i;//idx的右邊界下標-i下標
                }else{ //情況三
                    radius[i] = idx + radius[idx] - i;//至少
                    int count2 = 1;
                    //相等時,繼續擴展
                    while((i + radius[i] + count2) < len
                          && (i - radius[i] - count2) >= 0
                          && strb.charAt(i + radius[i] + count2) == strb.charAt(i - radius[i] - count2)){
                        count2++;
                    }
                    //不等時
                    radius[i] += (count2 - 1);
                    //更新最長迴文子串中心和右邊界下一個字符下標
                    if(i + radius[i] >= rad){
                        idx = i;
                        rad = i + count2;
                    }
                }
                //更新最長迴文子串的中心
                maxIdx = (radius[i] > radius[maxIdx] ? i : maxIdx);
            }
        }        
        StringBuilder ret = new StringBuilder();
        for(int i = maxIdx-radius[maxIdx]+1; i <= maxIdx + radius[maxIdx]; i+=2){
            ret.append(strb.charAt(i));
        }
        return ret.toString();
    }
	
	
	//求最長迴文子串長度(代碼優化)
	public static int maxLcpsLength(String str) {
		if(str == null || str.length() == 0) {
			return 0;
		}
		
		char[] charArr = manacherString(str);  //每個字符前後加#
		int[] pArr = new int[charArr.length];  //迴文半徑數組
		int index = -1;
		int pR = -1;
		int max = Integer.MIN_VALUE;
		for (int i = 0; i != charArr.length; i++) {
			//i在迴文右邊界裏面,我們起碼有一部分不用驗的區域,否則只有自己不用驗
			pArr[i] = pR > i ? Math.min(pArr[2 * index - i], pR - i) : 1;
			//跳過不用驗的區域,我們讓它往後擴一下
			while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {
				if (charArr[i + pArr[i]] == charArr[i - pArr[i]])  //如果相等,半徑加1
					pArr[i]++;
				else {  //否則跳出
					break;
				}
			}
			if (i + pArr[i] > pR) {  //如果迴文半徑超過右邊界
				pR = i + pArr[i];  //更新迴文半徑
				index = i;
			}
			max = Math.max(max, pArr[i]);  //取較大值
		}
		return max - 1;
	}
	
	public static char[] manacherString(String str) {
		char[] charArr = str.toCharArray();
		char[] res = new char[str.length() * 2 + 1];
		int index = 0;
		for(int i = 0; i != res.length; i++) {
			res[i] = (i & 1) == 0 ? '#' : charArr[index++];  //奇數位置設爲#,偶數爲原字符
		}
		return res;
	}
	
	public static void main(String[] args) {
		String str = "abc1234321ab";
		System.out.println(longestPalindrome(str));
		System.out.println(maxLcpsLength(str));
	}
}

3、Manacher算法應用

  給定一個字符串,只能往字符串後添加字符,如何讓字符串添加後整體爲迴文串,且添加的字符最少

解題思路:

  • 求得包含原字符串最後一個字符的最長迴文子串
  • 然後將原字符串中前面的字符逆序過來,就是答案

例如:

  • str:abc12321
  • 字符1的最長迴文子串是 12321,剩下 abc 逆序過來,cba就是答案

在具體的計算中,Manacher算法在計算3的迴文子串的時候,它的右邊界正好到最後一個字符,停止計算

Java代碼:

package day07;

public class Code05_Manacher_ShortestEnd {
	public static char[] manacherString(String str) {
		char[] charArr = str.toCharArray();
		char[] res = new char[str.length() * 2 + 1];
		int index = 0;
		for (int i = 0; i != res.length; i++) {
			res[i] = (i & 1) == 0 ? '#' : charArr[index++];
		}
		return res;
	}

	public static String shortestEnd(String str) {
		if (str == null || str.length() == 0) {
			return null;
		}
		char[] charArr = manacherString(str);
		int[] pArr = new int[charArr.length];
		int index = -1;
		int pR = -1;
		int maxContainsEnd = -1;
		for (int i = 0; i != charArr.length; i++) {
			pArr[i] = pR > i ? Math.min(pArr[2 * index - i], pR - i) : 1;
			while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {
				if (charArr[i + pArr[i]] == charArr[i - pArr[i]])
					pArr[i]++;
				else {
					break;
				}
			}
			if (i + pArr[i] > pR) {
				pR = i + pArr[i];
				index = i;
			}
			//迴文子串的右邊界到字符串的最後位置,停止計算
			if (pR == charArr.length) {
				maxContainsEnd = pArr[i];
				break;
			}
		}
		char[] res = new char[str.length() - maxContainsEnd + 1];
		for (int i = 0; i < res.length; i++) {
			res[res.length - 1 - i] = charArr[i * 2 + 1];
		}
		return String.valueOf(res);
	}

	public static void main(String[] args) {
		String str = "abcd123321";
		System.out.println(shortestEnd(str));

	}
}

三、BFPRT算法

  BFPRT算法解決的是在一個無序數組中找到第K大或第K小的數。當然這個問題可以先排序,時間複雜度爲 O(NlogN)O(N*logN),用BFPRT算法的時間複雜度爲 O(N)

在介紹BFPRT算法之前我們先來了解荷蘭國旗問題:

  給定一個數組arr,和一個數num,請把小於num的數放在數組的左邊,等於num的數放在數組的中間,大於num的數放在數組的右邊。

要求額外空間複雜度O(1),時間複雜度O(N)

在這裏插入圖片描述

Java代碼:

package day07;

public class Code06_NetherlandsFlag {
	public static int[] netherlandsFlag(int[] arr, int l, int r, int num) {
		int less = l - 1;
		int more = r + 1;
		while(l < more) {
			if(arr[l] < num) {
				swap(arr, ++less, l++);
			}else if(arr[l] > num) {
				swap(arr, --more, l);
			}else {
				l++;
			}
		}
		return new int[] {less + 1, more -1};
	}
	
	public static void swap(int[] arr, int i, int j) {
		int tmp = arr[i];
		arr[i] = arr[j];
		arr[j] = tmp;
	}
	
	// for test
	public static int[] generateArray() {
		int[] arr = new int[10];
		for (int i = 0; i < arr.length; i++) {
			arr[i] = (int) (Math.random() * 3);
		}
		return arr;
	}

	// for test
	public static void printArray(int[] arr) {
		if (arr == null) {
			return;
		}
		for (int i = 0; i < arr.length; i++) {
			System.out.print(arr[i] + " ");
		}
		System.out.println();
	}

	public static void main(String[] args) {
		int[] test = generateArray();

		printArray(test);
		int[] res = netherlandsFlag(test, 0, test.length - 1, 1);
		printArray(test);
		System.out.println(res[0]);
		System.out.println(res[1]);

	}
}

通過荷蘭國旗問題將數組分爲三部分:小於、等於和大於

在這裏插入圖片描述

在選擇劃分值num,BFPRT算法是如何進行的呢?

  • (1)求 BFPRT(arrk)BFPRT(arr,k) ,先對數組進行分組,相連的5個數一組,0~4,5~9,10~14,15~19,...最後不夠的單獨成一組
  • (2)對每組的5個數進行排序,跨組之間不排序,每組 O(1)O(1) 的複雜度,一共差不多 N5\frac{N}{5} 組,此步驟時間複雜度 O(N)O(N)
  • (3)取每個組的中位數構成新的數組,長度爲 N5\frac{N}{5}k=/2+1k&#x27;=長度/2+1
  • (4)遞歸調用BFPRT算法,傳入(3)中的新數組,BFPRT(k)BFPRT(新數組,k&#x27;),即求新數組的第 kk&#x27; 小的數
  • (5)根據num劃分

在這裏插入圖片描述

那麼爲什麼要選這樣的num值呢?

在這裏插入圖片描述

BFPRT算法複雜度(瞭解):
T(N)=T(N5)+T(7N10)+O(N)T(N)=T(\frac{N}{5})+T(\frac{7N}{10})+O(N)
時間複雜度是 O(N)O(N)

Java代碼:

package day07;

public class Code07_BFPRT_GetKMinNum {
	//主函數
	public static int[] getKMinNum(int[] arr, int k) {
		//越界返回原數組
		if(k < 1 || k > arr.length) {
			return arr;
		}
		int minKth = getKthMinNum(arr, k);  //獲取第k小的數
		int[] res = new int[k];  //結果數組,用於存儲前k個小的數
		int index = 0;
		for(int i = 0; i != arr.length; i++) {
			if(arr[i] < minKth) {  //小於minKth,存入res數組
				res[index++] = arr[i];
			}
		}
		for(;index != res.length; index++) {  //加入第k小的數
			res[index] = minKth;
		}
		return res;
	}
	
	public static int getKthMinNum(int[] arr, int k) {
		int[] copyArr = copyArray(arr);  //數組拷貝
		return bfprt(copyArr, 0, copyArr.length - 1, k - 1);
	}
	
	public static int[] copyArray(int[] arr) {
		int[] res = new int[arr.length];
		for (int i = 0; i != res.length; i++) {
			res[i] = arr[i];
		}
		return res;
	}

	public static int bfprt(int[] arr, int begin, int end, int k) {
		if (begin == end) {
			return arr[begin];
		}
		int num = medianOfMedians(arr, begin, end);  //求用於劃分的num值
		int[] pivotRange = partition(arr, begin, end, num);  //按num劃分小於,等於,大於區域,返回等於區域的左右下標
		if (k >= pivotRange[0] && k <= pivotRange[1]) {  //第k小的數在等於區域,直接返回等於區域的值
			return arr[k];
		} else if (k < pivotRange[0]) {  //第k小的數在小於區域,用小於區域繼續遞歸
			return bfprt(arr, begin, pivotRange[0] - 1, k);
		} else {
			return bfprt(arr, pivotRange[1] + 1, end, k); //第k小的數在大於區域,用大於區域繼續遞歸
		}
	}

	public static int medianOfMedians(int[] arr, int begin, int end) {
		int num = end - begin + 1;  //數的個數
		int offset = num % 5 == 0 ? 0 : 1;  //最後一組是否正好5個數
		int[] mArr = new int[num / 5 + offset];   //組數組,用於存每組的中位數
		for (int i = 0; i < mArr.length; i++) {
			int beginI = begin + i * 5;  //每個數組的起始下標
			int endI = beginI + 4;  //結束下標
			mArr[i] = getMedian(arr, beginI, Math.min(end, endI));  //獲取中位數存入組數組
		}
		return bfprt(mArr, 0, mArr.length - 1, mArr.length / 2);  //中位數數組遞歸調用bfprt
	}
	
	//獲取中位數
	public static int getMedian(int[] arr, int begin, int end) {
		insertionSort(arr, begin, end);  //組內排序,插入排序
		int sum = end + begin;
		int mid = (sum / 2) + (sum % 2);
		return arr[mid];  //返回中位數
	}
	
	//插入排序
	public static void insertionSort(int[] arr, int begin, int end) {
		for (int i = begin + 1; i != end + 1; i++) {
			for (int j = i; j != begin; j--) {
				if (arr[j - 1] > arr[j]) {
					swap(arr, j - 1, j);
				} else {
					break;
				}
			}
		}
	}
	
	//劃分區域
	public static int[] partition(int[] arr, int begin, int end, int pivotValue) {
		int less = begin - 1;
		int cur = begin;
		int more = end + 1;
		while (cur != more) {
			if (arr[cur] < pivotValue) {
				swap(arr, ++less, cur++);
			} else if (arr[cur] > pivotValue) {
				swap(arr, cur, --more);
			} else {
				cur++;
			}
		}
		int[] range = new int[2];
		range[0] = less + 1;
		range[1] = more - 1;
		return range;
	}
	
	//交換
	public static void swap(int[] arr, int index1, int index2) {
		int tmp = arr[index1];
		arr[index1] = arr[index2];
		arr[index2] = tmp;
	}
	//打印數組
	public static void printArray(int[] arr) {
		for (int i = 0; i != arr.length; i++) {
			System.out.print(arr[i] + " ");
		}
		System.out.println();
	}

	public static void main(String[] args) {
		int[] arr = { 6, 9, 4, 3, 1, 2, 2, 5, 6, 1, 3, 5, 9, 7, 2, 5, 6, 1, 9 };
		// sorted : { 1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 5, 5, 6, 6, 6, 7, 9, 9, 9 }
		printArray(getKMinNum(arr, 10));
	}
}


本題還可以使用堆來實現,不過時間複雜度爲 O(NlogK)O(NlogK)

在這裏插入圖片描述

package day07;

public class Code08_Heap_GetKMinNum {
	
	public static int[] getKMinNum(int[] arr, int k) {
		//邊界
		if (k < 1 || k > arr.length) {
			return arr;
		}
		int[] kHeap = new int[k];  //新建長度爲k的數組
		for (int i = 0; i != k; i++) {  //前k個數建立大根堆
			heapInsert(kHeap, arr[i], i);
		}
		for (int i = k; i != arr.length; i++) { //從k位置開始遍歷數組
			if (arr[i] < kHeap[0]) {  //如果當前遍歷小於堆頂,進行下沉操作
				kHeap[0] = arr[i];
				heapify(kHeap, 0, k);
			}
		}
		return kHeap;  //返回數組
	}

	//大根堆
	public static void heapInsert(int[] arr, int value, int index) {
		arr[index] = value;
		while (index != 0) {
			int parent = (index - 1) / 2;  //父節點
			if (arr[parent] < arr[index]) {
				swap(arr, parent, index);
				index = parent;
			} else {
				break;
			}
		}
	}

	//堆下沉操作
	public static void heapify(int[] arr, int index, int heapSize) {
		int left = index * 2 + 1;   //當前節點的左節點
		int right = index * 2 + 2;  //當前節點的右節點
		int largest = index;  //較大的下標
		while (left < heapSize) {  //沒到邊界
			if (arr[left] > arr[index]) {  //如果左大於當前
				largest = left;  //更新較大下標
			}
			if (right < heapSize && arr[right] > arr[largest]) {  //右大於剛纔的較大值
				largest = right;  //較大下標更新爲右
			}
			if (largest != index) {  //如果較大的數不是當前數,進行交換
				swap(arr, largest, index);
			} else {
				break;
			}
			index = largest;  //當前遍歷到較大數位置
			left = index * 2 + 1;  //更新left
			right = index * 2 + 2;  //更新right
		}
	}
	public static void swap(int[] arr, int index1, int index2) {
		int tmp = arr[index1];
		arr[index1] = arr[index2];
		arr[index2] = tmp;
	}

	public static void printArray(int[] arr) {
		for (int i = 0; i != arr.length; i++) {
			System.out.print(arr[i] + " ");
		}
		System.out.println();
	}

	public static void main(String[] args) {
		int[] arr = { 6, 9, 4, 3, 1, 2, 2, 5, 6, 1, 3, 5, 9, 7, 2, 5, 6, 1, 9 };
		// sorted : { 1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 5, 5, 6, 6, 6, 7, 9, 9, 9 }
		printArray(getKMinNum(arr, 10));

	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章