經典算法-並查集、快速排序、字典序算法、二分搜索、牛頓開方法、求質數(篩選法)、編輯距離、滑動窗口、異或求重、長除法

目錄

​​​​​​​​​​​​​​

並查集

快速排序

字典序算法

二分搜索

開根號-牛頓開方法

求質數

編輯距離

滑動窗口

異或求重

長除法


​​​​​​​

並查集

並查集用於解決相同元素集合動態連接的快速構建算法

例:求相等集合a=b,e=d,d=b

初始時,ab爲一集合,ed爲一集合,又d=b,故應將abed變爲一集合,之後e=a就是顯而易見的事了

並查集利用構建樹形結構的方式,每個元素有上級,上級有上級,最高級的上級爲其本身,通過查看兩個元素的最高級是否相同來判斷兩元素是否屬於同一集合

即,每個集合爲一棵樹,判斷兩個元素是否屬於同一個集合只用判斷其樹根是否相同即可

初始時有數組pre[],pre[i]表示元素i的上一級

find(int x)用於找出元素x的最高級,即樹根

  1.  int find(int x){
  2.             while(x!=pre[x]){
  3.                 x=pre[x];
  4.             }   
  5.             return x;
  6.         }

union(int x,int y)用於將x,y所代表的兩個集合合併

  1.  void union(int x,int y){
  2.             int a=find(x);
  3.             int b=find(y);
  4.             pre[b]=a;
  5.         }

例題(leetcode-990):

給定一個由表示變量之間關係的字符串方程組成的數組,每個字符串方程 equations[i] 的長度爲 4,並採用兩種不同的形式之一:"a==b" 或 "a!=b"。在這裏,a 和 b 是小寫字母(不一定不同),表示單字母變量名。

只有當可以將整數分配給變量名,以便滿足所有給定的方程時才返回 true,否則返回 false。 

示例 1:

輸入:["a==b","b!=a"]
輸出:false
解釋:如果我們指定,a = 1 且 b = 1,那麼可以滿足第一個方程,但無法滿足第二個方程。沒有辦法分配變量同時滿足這兩個方程。

示例 3:

輸入:["a==b","b==c","a==c"]
輸出:true

示例 5:

輸入:["c==c","b==d","x!=z"]
輸出:true

提示:

  1. 1 <= equations.length <= 500
  2. equations[i].length == 4
  3. equations[i][0] 和 equations[i][3] 是小寫字母
  4. equations[i][1] 要麼是 '=',要麼是 '!'
  5. equations[i][2] 是 '='
class Solution {

        //使用一下傳統並查集

        private  int[] pre=new int[2560];//存儲其上一級,最高級爲其本身,成爲代理人

        //發現x所在集合的最高代理人

        private int find(int x){

            while(x!=pre[x]){

                x=pre[x];

            }   

            return x;

        }

        //兩者應該屬於同一夥,合併,爲使其成爲樹狀結構,加快查找速度,將最高代理人合併

        private void union(int x,int y){

            int a=find(x);

            int b=find(y);

            if(a!=b){

                pre[b]=a;

            }

        }

    public boolean equationsPossible(String[] equations) {

       for(int i=0;i<2560;i++){

            pre[i]=i;//初始化

        }

        

        for(String s:equations){

            int l=s.charAt(0);

            int r=s.charAt(3);

            if(s.charAt(1)=='='&&find(l)!=find(r)){

                union(l,r);

            }

        }

        for(String s:equations){

            int l=s.charAt(0);

            int r=s.charAt(3);

            if(s.charAt(1)=='!'&&find(l)==find(r)){

                return false;

            }

        }

        return true;

    }

}

參考:https://blog.csdn.net/liujian20150808/article/details/50848646

快速排序

快速排序以分治法爲基本思想,遞歸爲基本表現形式。

對於一個數組nums,傳遞參數left,right,爲範圍,以第一個數(nums[0])爲key,將所有<=key的數放在左邊,所有>key的數放在右邊。

i=left,j=right,向前推進i直到找到第一個>key的數,向後推進直到找到第一個<=key的數

交換i,j的數的位置直到i==j

退出循環,此時i==j,覈查nums[i]與key,保證<=i的數全部<=key,>j的數全部>j。因爲以nums[0]爲基準,nums[0]不能在參加遞歸運算,交換nums[i]與nums[0],繼續遞歸,範圍爲[left,i-1],[j,right]

遞歸的結束條件即爲left>=right

    private void sort(int[] nums,int left,int right){

        if(left>=right)return ;

        int i=left,j=right;

        int key=nums[left];

        while(i<j){

            while(i<j&&nums[j]>key){

                j--;

            }

            while(i<j&&nums[i]<=key){

                i++;

            }

            int tmp=nums[i];

            nums[i]=nums[j];

            nums[j]=tmp;

        }

        if(nums[i]<=key)j++;

        else i--;

        nums[left]=nums[i];

        nums[i]=key;//基準數不再參與數據處理

        sort(nums,left,i-1);

        sort(nums,j,right);

    }

字典序算法

字典序即按照a-zA-Z0-9的順序對由這些元素組成的串的從小到大的全排列。

如[1,2,3]進行全排列,將1,2,3組成數字,字典序就相當於按所有組成數字從小到大進行排列。

123->132->213->231->312->321

字典序算法可用於全排列(n!)生成

字典序算法的步驟爲:

  • 1.從右至左找到第一個左鄰小於右鄰,記錄左鄰位置i
  • 2.從右至左找到第一個大於左鄰的數,即位置爲j,交換num[i],num[j]
  • 3.對位置i之後的所有數進行排列

例題(leetcode-31)

實現獲取下一個排列的函數,算法需要將給定數字序列重新排列成字典序中下一個更大的排列。

如果不存在下一個更大的排列,則將數字重新排列成最小的排列(即升序排列)。

必須原地修改,只允許使用額外常數空間。

以下是一些例子,輸入位於左側列,其相應輸出位於右側列。
1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1

class Solution {
    public void nextPermutation(int[] nums) {
        //考察字典序算法,字典序可用於生成全排列
        //字典序算法步驟爲:
        //1.從右至左找到第一個左鄰小於右鄰,記錄左鄰位置i
        //2.從右至左找到第一個大於左鄰的數,即位置爲j,交換num[i],num[j]
        //3.對位置i之後的所有數進行排列
        //如果沒找到i則說明已經是最大的排列數
        int a=-1;
        //1.從右至左找到第一個左鄰小於右鄰,記錄左鄰位置i
        for(int i=nums.length-1;i>0;i--){
            if(nums[i-1]<nums[i]){
                a=i-1;
                break;
            }
        }
        if(a==-1){
            sort(nums,0,nums.length-1);
            return ;
        }
        //2.從右至左找到第一個大於左鄰的數,即位置爲j,交換num[i],num[j]
        for(int i=nums.length-1;i>a;i--){
            if(nums[i]>nums[a]){
                int temp=nums[i];
                nums[i]=nums[a];
                nums[a]=temp;
                sort(nums,a+1,nums.length-1);
                return ;
            }
        }
    }
    private void sort(int[] nums,int left,int right){
        if(left>=right)return ;
        int i=left,j=right;
        int key=nums[left];
        while(i<j){
            while(i<j&&nums[j]>key){
                j--;
            }
            while(i<j&&nums[i]<=key){
                i++;
            }
            int tmp=nums[i];
            nums[i]=nums[j];
            nums[j]=tmp;
        }
        if(nums[i]<=key)j++;
        else i--;
        nums[left]=nums[i];
        nums[i]=key;//基準數不再參與數據處理
        sort(nums,left,i-1);
        sort(nums,j,right);
    }
}

二分搜索

太經典了,看代碼即可,nums爲已排序數組

        int left=0,right=nums.length-1;
        while(left<=right){
            int med=(left+right)/2;
            if(nums[med]==target)return med;
            else if(nums[med]<target){
                left=med+1;
            }else{
                right=med-1;
            }
        }

開根號-牛頓開方法

實際上就是完成一個sqrt()函數,計算機完成開根號的運算,大部分使用牛頓迭代法。

假設a爲要開方的數,那麼x就是答案,即求f(x)=0時,x爲何值

選取函數上一點(t,t^2-a),對於該點,求切線,切線上在x軸方向上的零點必定逼近於結果

切線的斜率對原函數求導即可,k=2t,該切線經過點(t,t^2-a),求切線在x軸方向上的零點,即(t^2+a)/2t

以該點繼續迭代即可不斷逼近與結果

    public int mySqrt(int x) {

        //牛頓迭代法

        //y=res^2-x,選取點(a,a^2-x)做切點

        //斜率即求導數k=2a,令y=2ares`+t

        //帶入化簡,零點爲res`=(a^2+x)/2a,繼續迭代

        if(x==0)return 0;

        if(x<=3)return 1;

        long current=x/2;

        while(current*current>x){

            current=((current+x/current)/2);

        }

        return (int)current;

    }

當然,原理上是這麼算的,實際上想要運行的更快還有一些技巧

關於這個方法,還有一個神奇的數字

0x5f3759df

​雷神之錘

參看:https://www.cnblogs.com/pkuoliver/archive/2010/10/06/1844725.html

求質數

有兩種方法

1.暴力求解,對每一個數進行判斷,判斷到sqrt(n)

for(int i=2;i<=n;i++){

    for(int j=2;j<=sqrt(i);j++){

        if(i%j==0)==>i不是質數;break;

    }

}

2.篩選法,對於2來說,2的倍數一定不是質數,對於3來說,3的倍數一定不是質數。。開一個int[n]的數組,對於2,3...n

2是質數,但2的倍數不是,打表,每次先判斷當前是否被打上去,如果是直接跳過,否則是質數,打表。

 boolean[] check=new boolean[n+1]
 int tot = 0;
 for (int i = 2; i <= n; ++i)
{
   if (!check[i])
 {
    prime[tot++] = i;
  }
 for (int j = i+i; j <= n; j += i)
  {
    check[j] = true;
  }
 }

編輯距離

        有兩個具有相同性質的物質可以互相轉換,字符串A轉換成字符串B,A,B都由字符構成,給定基本條件,求A能轉換成B所需要的最小步驟,這個問題比較抽象,而所用的就是編輯距離算法,實際上還是動態規劃算法。

        編輯距離是針對兩個字符串(例如英文字)的差異程度的量化量測,量測方式是看至少需要多少次的處理才能將一個字符串變成另一個字符串。編輯距離可以用在自然語言處理中,例如拼寫檢查可以根據一個拼錯的字和其他正確的字的編輯距離,判斷哪一個(或哪幾個)是比較可能的字。DNA也可以視爲用A、C、G和T組成的字符串,因此編輯距離也用在生物信息學中,判斷二個DNA的類似程度。(來自百度百科)

例題(leetcode-72)

給定兩個單詞 word1 和 word2,計算出將 word1 轉換成 word2 所使用的最少操作數 。

你可以對一個單詞進行如下三種操作:

插入一個字符
刪除一個字符
替換一個字符
示例 1:

輸入: word1 = "horse", word2 = "ros"
輸出: 3
解釋: 
horse -> rorse (將 'h' 替換爲 'r')
rorse -> rose (刪除 'r')
rose -> ros (刪除 'e')

假設dp[i][j]表示前i個字符轉換成前j個字符所需要的最短距離

對於當前字符word1[i]與word2[j]來說,

如果word[i]==word[j],顯然 dp[i][j]=dp[i-1][j-1]

否則,有三種方法可以對當前字符進行處理。

  • 1.在前i個字符前插入一個字符,顯然我們不知道插到哪裏合適,但是如果插入一個字符與word2[j]相同,那麼就至少是最小變換中的一種,因爲如果插入到別的地方,當前字符還是和word2[j]不同,還要執行變換操作,而插入字符放在最前面,那麼dp[i][j]=1+dp[i][j-1](因爲插入字符和word2[j]相同)
  • 2.在前i個字符中刪除一個字符,顯然,刪誰都是可以的,但是因爲word1[i]和word2[j]不相同,那麼刪除word1[i]至少是最小變換中的一種,即dp[i][j]=1+dp[i-1][j]
  • 3.替換一個字符,替換word1[i]=word2[j],則dp[i][j]=1+dp[i-1][j-1]

由以上推斷就可以寫動歸代碼,注意底層條件的初始設置。

class Solution {
    public int minDistance(String word1, String word2) {

        int[][] dp=new int[word1.length()+1][word2.length()+1];
        //初始化底層條件
        for(int i=1;i<=word1.length();i++){
            dp[i][0]=i;
        }
        for(int i=1;i<=word2.length();i++){
            dp[0][i]=i;
        }
        
        for(int i=0;i<word1.length();i++){
            for(int j=0;j<word2.length();j++){
                if(word1.charAt(i)==word2.charAt(j)){
                    dp[i+1][j+1]=dp[i][j];
                }else{
                    dp[i+1][j+1]=1+Math.min(Math.min(dp[i][j+1],dp[i+1][j]),dp[i][j]);
                }
            }
        }
        
        return dp[word1.length()][word2.length()];
    }
}

滑動窗口

這是一個非常經典的思想,常用於在給定的範圍中尋求包含的子範圍。

滑動窗口維護兩個指針:left,right

最開始left和right都處於最左邊,right向右滑動擴大窗口直到子窗口[left...right]已滿足條件

這時就要開始動左指針left,對於left當前狀態對應的元素,判斷是否能將其拋出,如果能left向右滑動,即縮小窗口,同時再次進行判斷,如果不能則動右指針right擴大窗口。簡而言之,right擴大窗口,left縮小窗口,整個窗口一次遍歷時就能遍歷到所有符合條件的子窗口。

leetcode官方描述:

  • 初始,left指針和right指針都指向SS的第一個元素.
  • 將 right 指針右移,擴張窗口,直到得到一個可行窗口,亦即包含TT的全部字母的窗口。
  • 得到可行的窗口後,將left指針逐個右移,若得到的窗口依然可行,則更新最小窗口大小。
  • 若窗口不再可行,則跳轉至 22。

例題(leetcode-76)

給你一個字符串 S、一個字符串 T,請在字符串 S 裏面找出:包含 T 所有字母的最小子串。

示例:

輸入: S = "ADOBECODEBANC", T = "ABC"
輸出: "BANC"
說明:

如果 S 中不存這樣的子串,則返回空字符串 ""。
如果 S 中存在這樣的子串,我們保證它是唯一的答案。

class Solution {
    public String minWindow(String s, String t) {
        //暴力法O(n*2*m),超時
        //使用滑動窗口,left縮減窗口,right擴大窗口,窗口大小可動態變化
        Map<Character,Integer>t2=new HashMap<>();
        for(char c:t.toCharArray()){
            t2.put(c,t2.getOrDefault(c,0)+1);
        }
        if(!check(getMap(s),t2))return ""; 
        
        int left=0;
        int right=t.length()-1;
        int ls=0;
        int rs=s.length()-1;
        //t1也跟着動態變化
        Map<Character,Integer>t1=new HashMap<>();
        for(char c:s.substring(0,t.length()).toCharArray()){
            t1.put(c,t1.getOrDefault(c,0)+1);
        }

        while(right<s.length()){
            if(check(t1,t2)){
                //滑動窗口包含t,收縮滑動窗口
                if(right-left<rs-ls){
                    rs=right;
                    ls=left;
                }
                t1.put(s.charAt(left),t1.get(s.charAt(left))-1);
                left++;
                continue;
            }
            //不包含,擴展滑動窗口
            right++;
            if(right<s.length())t1.put(s.charAt(right),t1.getOrDefault(s.charAt(right),0)+1);
        }

        return s.substring(ls,rs+1);
    }
    
    private Map<Character,Integer> getMap(String s){
        Map<Character,Integer>t1=new HashMap<>();
        for(char c:s.toCharArray()){
            t1.put(c,t1.getOrDefault(c,0)+1);
        }
        return t1;
    }
    
    //t1完全包含t2
    private boolean check(Map<Character,Integer>t1,Map<Character,Integer>t2){
        for(char c:t2.keySet()){
            if(!t1.containsKey(c))return false;
            if(t1.get(c)<t2.get(c))return false;
        }
        return true;
    }
}

異或求重

    異或的性質:

    1、交換律:a^b = b^a;

    2、結合律:(a^b)^c = a^(b^c);

    3、對於任意的a:a^a=0,a^0=a,a^(-1)=~a

        由以上性質可以得出一個重要推導:a^b^c^d^a^k = b^c^d^k^(a^a) = b^c^d^k^0 = b^c^d^k,即如果有多個數異或,其中有重複的數,則無論這些重複的數是否相鄰,都可以根據異或的性質將其這些重複的數消去。如果有偶數個數則異或後爲0,如果爲奇數個,則異或後只剩一個。(引用於:https://blog.csdn.net/ns_code/article/details/27568975

 1、1-1000放在含有1001個元素的數組中,只有唯一的一個元素值重複,其它均只出現一次。每個數組元素只能訪問一次,設計一個算法,將它找出來;不用輔助存儲空間,能否設計一個算法實現?

    當然,這道題,可以用最直觀的方法來做,將所有的數加起來,減去1+2+3+...+1000的和,得到的即是重複的那個數,該方法很容易理解,而且效率很高,也不需要輔助空間,唯一的不足時,如果範圍不是1000,而是更大的數字,可能會發生溢出。

    我們考慮用異或操作來解決該問題。現在問題是要求重複的那個數字,我們姑且假設該數字式n吧,如果我們能想辦法把1-1000中除n以外的數字全部異或兩次,而數字n只異或一次,就可以把1-1000中出n以外的所有數字消去,這樣就只剩下n了。我們首先把所有的數字異或,記爲T,可以得到如下:

T = 1^2^3^4...^n...^n...^1000 = 1^2^3...^1000(結果中不含n)

    而後我們再讓T與1-1000之間的所有數字(僅包含一個n)異或,便可得到該重複數字n。如下所示:

T^(a^2^3^4...^n...^1000) = T^(T^n) = 0^n = n

    2、一個數組中只有一個數字出現了一次,其他的全部出現了兩次,求出這個數字。

    明白了上面題目的推導過程,這個就很容易了,將數組中所有的元素全部異或,最後出現兩次的元素會全部被消去,而最後會得到該只出現一次的數字。

長除法

用於大精度除法求小數

模擬除法中,小數上的商可以重複,或陷入無限循環(小數循環節),而商實際上是由上一次剩下的模數來決定的,商可以重複而模數不能重複,一旦模數開始重複就代表循環節開始出現

例題:leetcoed分數到小數

給定兩個整數,分別表示分數的分子 numerator 和分母 denominator,以字符串形式返回小數。

如果小數部分爲循環小數,則將循環的部分括在括號內。

示例 1:

輸入: numerator = 1, denominator = 2
輸出: "0.5"

示例 2:

輸入: numerator = 2, denominator = 1
輸出: "2"

示例 3:

輸入: numerator = 2, denominator = 3
輸出: "0.(6)"
class Solution {
    public String fractionToDecimal(int numerato, int denominato) {
        //模擬除法,由題意判斷不會出現無限不循環小數
        //***********模擬長除法:(商的)小數循環節出現則每次除下的模數也會重複出現
        //原理:模數一旦重複則代表要出現循環節
        if(numerato==0)return "0";
        char flag='+';
        if((numerato>0&&denominato<0)||(numerato<0&&denominato>0))flag='-';
        
        long numerator=Math.abs((long)numerato);//不用long會溢出
        long denominator=Math.abs((long)denominato);
        String res="";
        //能夠整除
        if(numerator>=denominator&&numerator%denominator==0){
            return flag=='+'?String.valueOf(numerator/denominator):'-'+String.valueOf(numerator/denominator);
        }
        
        //不能夠整除,注意符號

        Map<Long,Integer>map=new HashMap<>();//記錄模數與每次除下的商所在的位置
        
        res+=numerator/denominator+".";
        numerator%=denominator;
        while(true){
            //結束條件:能夠除盡或者陷入無限循環(模數重複出現)
            if(map.containsKey(numerator)){
                res=res.substring(0,map.get(numerator))+"("+res.substring(map.get(numerator))+")";
                break;
            }
            map.put(numerator,res.length());//放模數
                
            numerator*=10;
            res+=numerator/denominator;
            //System.out.println(numerator+" "+denominator);
            numerator%=denominator;//模數更新
            if(numerator==0){
                break;
            }
        }
        return flag=='+'?res:'-'+res;
    }
    
}

注意對於int除法來說爲保證能求得準確精度應該使用long類型,否則模數一直*10後面可能會溢出

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