算法 | 一周刷完《劍指Offer》 Day1:第1~16題

開個新坑,準(zhǔn)備校招研發(fā)崗面試,基本的算法還是要過關(guān)的。


寫在前面

下一篇:算法 | 一周刷完《劍指Offer》 Day2:第17~26題


Day1:第1~16題

后面總會遇到難的繞的題,前兩天多做幾道,總不會虧的。

  • T1. 二維數(shù)組中的查找
  • T2. 替換空格
  • T3. 從尾到頭打印鏈表
  • T4. 重建二叉樹
  • T5. 用兩個棧實現(xiàn)隊列
  • T6. 旋轉(zhuǎn)數(shù)組的最小數(shù)字
  • T7. 斐波那契數(shù)列
  • T8. 跳臺階
  • T9. 變態(tài)跳臺階
  • T10. 矩陣覆蓋
  • T11. 二進(jìn)制中 1 的個數(shù)
  • T12. 數(shù)值的整數(shù)次方
  • T13. 調(diào)整數(shù)組順序使奇數(shù)位于偶數(shù)前面
  • T14. 鏈表中倒數(shù)第 K 個結(jié)點
  • T15. 反轉(zhuǎn)鏈表
  • T16. 合并兩個排序的鏈表

T1. 二維數(shù)組中的查找

題目描述

在一個二維數(shù)組中(每個一維數(shù)組的長度相同),每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數(shù),輸入這樣的一個二維數(shù)組和一個整數(shù),判斷數(shù)組中是否含有該整數(shù)。

解題思路

因為矩陣中的每一個數(shù),左邊都比它小,下邊都比它大。因此,從右上角開始查找,就可以根據(jù) target 和當(dāng)前元素的大小關(guān)系來縮小查找區(qū)間。

    public boolean Find(int target, int[][] array) {
        if(array == null || array.length == 0 || array[0].length == 0) {
            return false;
        }
        
        int m = 0, n = array[0].length - 1;//從右上角開始找,array[0][n-1]
        
        while(m <= array.length - 1 && n >= 0) {
            if(target == array[m][n])
                return true;
            else if(target > array[m][n])
                m ++;
            else
                n --;
        }
        
        return false;
    }

T2. 替換空格

題目描述

請實現(xiàn)一個函數(shù),將一個字符串中的每個空格替換成“%20”。例如,當(dāng)字符串為We Are Happy,則經(jīng)過替換之后的字符串為We%20Are%20Happy。

解題思路

由于每個空格替換成了三個字符,首先要擴(kuò)容字符串長度至替換后的長度,因此當(dāng)遍歷到一個空格時,需要在尾部填充兩個任意字符。

然后聲明兩個下標(biāo),一個為原字符串末尾下標(biāo) i,一個為現(xiàn)字符串末尾 j,兩個下標(biāo)同步從后往前掃。當(dāng) i 指向空格時,j 就倒著依次添加 '0', '2', '%'。
這樣做的目的是: j 下標(biāo)不會超過 i 下標(biāo),不會影響原字符串的內(nèi)容。

    public String replaceSpace(StringBuffer str) {
        int oldLen = str.length();
        
        for(int i = 0; i < oldLen; i ++) {
            if(str.charAt(i) == ' ') {//每出現(xiàn)一個空格,在結(jié)尾添加兩個任意字符,擴(kuò)充字符串長度
                str.append("12");
            }
        }
        
        int i = oldLen - 1;
        int j = str.length() - 1;
        
        while(i >= 0 && j > i) {
            char c = str.charAt(i);
            i --;
            
            if(c == ' ') {//每出現(xiàn)一個空格,替換為%20
                str.setCharAt(j, '0');
                j --;
                str.setCharAt(j, '2');
                j --;
                str.setCharAt(j, '%');
                j --;
            } else {//否則照舊
                str.setCharAt(j, c);
                j --;
            }
        }
        
        return str.toString();
    }

T3. 從尾到頭打印鏈表

題目描述

輸入一個鏈表,按鏈表值從尾到頭的順序返回一個ArrayList。

解題思路

使用棧實現(xiàn):先入后出。全部入棧,依次出棧,順序即為從尾到頭。

    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        //使用棧實現(xiàn),先入后出
        Stack<Integer> stack = new Stack<>();
        ArrayList<Integer> arrayList = new ArrayList<>();
        
        while(listNode != null) {
            stack.push(listNode.val);
            listNode = listNode.next;
        }
        
        while(!stack.isEmpty()) {
            arrayList.add(stack.pop());
        }
        
        return arrayList;
    }

使用遞歸實現(xiàn):先加入鏈表后面的結(jié)點,再加入當(dāng)前結(jié)點,順序即為從尾到頭。

    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        //使用遞歸實現(xiàn),先加入鏈表后面的結(jié)點,再加入當(dāng)前結(jié)點
        ArrayList<Integer> arrayList = new ArrayList<>();
        
        if(listNode != null) {
            arrayList.addAll(printListFromTailToHead(listNode.next));//先遞歸
            arrayList.add(listNode.val);//再加入當(dāng)前
        }
        
        return arrayList;
    }

T4. 重建二叉樹

題目描述

輸入某二叉樹的前序遍歷和中序遍歷的結(jié)果,請重建出該二叉樹。假設(shè)輸入的前序遍歷和中序遍歷的結(jié)果中都不含重復(fù)的數(shù)字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹并返回。

解題思路

首先清楚以下性質(zhì):
前序遍歷:根 -> 左 -> 右,中序遍歷:左 -> 根 -> 右。

因此一顆二叉樹中,前序遍歷第一個結(jié)點為根結(jié)點,通過它在中序遍歷中的位置,可以將中序遍歷分為兩部分,左半部分是該結(jié)點的左子樹右半部分是該結(jié)點的右子樹

根據(jù)這個原理,遞歸執(zhí)行即可重構(gòu)出二叉樹。

    private Map<Integer, Integer> map = new HashMap<>();//用于查找中序遍歷數(shù)組中,每個值對應(yīng)的索引
    
    public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
        for(int i = 0; i < in.length; i ++) {
            map.put(in[i], i);//key: in的值,value: 值在in中位置
        }
        
        return getBiTree(pre, 0, pre.length - 1, 
                            in, 0, in.length - 1);
    }
    
    private TreeNode getBiTree(int[] pre, int preLeft, int preRight, //前序遍歷及當(dāng)前在前序遍歷中的區(qū)間
                                int[] in, int inLeft, int inRight) { //中序遍歷及當(dāng)前在前序遍歷中的區(qū)間
        if(preLeft == preRight) { //即根據(jù)前序遍歷,當(dāng)前結(jié)點無子結(jié)點
            return new TreeNode(pre[preLeft]);
        }
        if(preLeft > preRight || inLeft > inRight) {
            return null;
        }
        
        TreeNode root = new TreeNode(pre[preLeft]);
        int inIndex = map.get(root.val);
        int leftTreeSize = inIndex - inLeft;//該結(jié)點左半部分的結(jié)點數(shù)
        
        //遞歸,獲取左子樹及右子樹
        root.left = getBiTree(pre, preLeft + 1, preLeft + leftTreeSize, //左半部分在前序遍歷中的區(qū)間
                                in, inLeft, inIndex - 1);//左半部分在中序遍歷中的區(qū)間
        root.right = getBiTree(pre, preLeft + 1 + leftTreeSize, preRight, //右半部分在前序遍歷中的區(qū)間
                                in, inIndex + 1, inRight);//右半部分在中序遍歷中的區(qū)間
        
        return root;
    }

T5. 用兩個棧實現(xiàn)隊列

題目描述

用兩個棧來實現(xiàn)一個隊列,完成隊列的Push和Pop操作。 隊列中的元素為int類型。

解題思路

隊列:先進(jìn)先出,:先進(jìn)后出

例如,假設(shè) 1 ~ n 的數(shù)字順序,入Stack1,出的順序為 n ~ 1 。

這時,Stack1出棧進(jìn)入Stack2,進(jìn)入Stack2的順序為n ~ 1,那么Stack2出的順序為 1 ~ n 。

相當(dāng)于隊列 1 ~ n 進(jìn), 1 ~ n 出。

    Stack<Integer> stack1 = new Stack<Integer>();//stack1入棧時使用
    Stack<Integer> stack2 = new Stack<Integer>();//stack2出棧時使用,直接出棧即可
    
    public void push(int node) {
        while(!stack2.isEmpty()) {//先將stack2中所有元素壓入stack1
            stack1.push(stack2.pop());
        }
        stack1.push(node);//然后將當(dāng)前要進(jìn)入隊列元素壓入stack1
        while(!stack1.isEmpty()) {//最后將stack1所有元素壓入stack2
            stack2.push(stack1.pop());//此時stack2中出棧順序即為隊列出棧順序,相當(dāng)于先入先出
        }
    }
    
    public int pop() {
        return stack2.pop();
    }

T6. 旋轉(zhuǎn)數(shù)組的最小數(shù)字

題目描述

把一個數(shù)組最開始的若干個元素搬到數(shù)組的末尾,我們稱之為數(shù)組的旋轉(zhuǎn)。 輸入一個非減排序的數(shù)組的一個旋轉(zhuǎn),輸出旋轉(zhuǎn)數(shù)組的最小元素。 例如數(shù)組{3,4,5,1,2}為{1,2,3,4,5}的一個旋轉(zhuǎn),該數(shù)組的最小值為1。 NOTE:給出的所有元素都大于0,若數(shù)組大小為0,請返回0。

解題思路

我們可以認(rèn)為數(shù)組分成了兩部分,前半部分是較大的數(shù),后半部分是較小的數(shù)

利用二分查找的思路,觀察取中的數(shù)在前半部分還是后半部分,以此來找出最小的數(shù)(后半部分的第一個數(shù))。

    public int minNumberInRotateArray(int[] array) {
        if(array.length == 0) {
            return 0;
        }
        
        //二分查找
        int low = 0, high = array.length - 1;
        while (array[low] >= array[high]) {
            if(high - low == 1) {
                return array[high];
            }
            int mid = low + (high - low) / 2;//取中
            if(array[mid] >= array[low]) {//此時mid在前半?yún)^(qū)大的數(shù)里
                low = mid;
            } else {//此時mid在后半?yún)^(qū)小的數(shù)里
                high = mid;
            }
        }
        return array[low];
    }

T7. 斐波那契數(shù)列

題目描述

大家都知道斐波那契數(shù)列,現(xiàn)在要求輸入一個整數(shù)n,請你輸出斐波那契數(shù)列的第n項(從0開始,第0項為0)。n<=39

解題思路

斐波那契數(shù)列:F[n]=F[n-1]+F[n-2] (n>=3,F[1]=1,F[2]=1)

注意:此題要求F[1]=0。

    public int Fibonacci(int n) {//n<=39
        int[] array = new int[40];
        array[0] = 0;
        array[1] = 1;
        for(int i = 2; i < array.length; i ++) {
            array[i] = array[i - 1] + array[i - 2];
        }
        
        return array[n];
    }

T8. 跳臺階

題目描述

一只青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法(先后次序不同算不同的結(jié)果)。

解題思路

動態(tài)規(guī)劃的思想。這里采用倒著往前推的方法遞減target,最后一次跳1或最后一次跳2,倒著往前遞歸。

    public int JumpFloor(int target) {
        if(target == 0) {
            return 0;
        }
        if(target == 1) {
            return 1;
        }
        if(target == 2) {
            return 2;
        }
        if(target > 2) {//遞歸求可能出現(xiàn)的情況總數(shù)(最后一次跳1或最后一次跳2,倒著往前推)
            return JumpFloor(target - 1) + JumpFloor(target - 2);
        }
        
        return 0;
    }

或者,其本質(zhì)上是斐波那契數(shù)列的原理。

    public int JumpFloor(int target) {
        if(target == 0) {
            return 0;
        }
        if(target == 1) {
            return 1;
        }

        int[] array = new int[target];
        array[0] = 1;
        array[1] = 2;
        for (int i = 2; i < target; i++) {
            array[i] = array[i - 1] + array[i - 2];
        }
        return array[target - 1];
    }

T9. 變態(tài)跳臺階

題目描述

一只青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。

解題思路

同理,動態(tài)規(guī)劃的思想,遞歸求解。(這次情況有點多,用到循環(huán)了)

    public int JumpFloorII(int target) {
        if(target == 0 || target == 1) {
            return 1;
        }
        if(target == 2) {
            return 2;
        }
        
        int sum = 0;
        for(int i = 0; i < target; i ++) {//本次跳0次到跳target-1次
            sum += JumpFloorII(i);//對于本次的跳躍又有多少種跳法?遞歸獲取結(jié)果
        }
        
        return sum;
    }

T10. 矩陣覆蓋

題目描述

我們可以用 2 * 1 的小矩形橫著或者豎著去覆蓋更大的矩形。請問用n個 2 * 1 的小矩形無重疊地覆蓋一個 2 * n 的大矩形,總共有多少種方法?

解題思路

原理同T8,詳見圖片。

    public int RectCover(int target) {
        if(target == 0) {
            return 0;
        }
        if(target == 1) {
            return 1;
        }

        int[] array = new int[target];
        array[0] = 1;
        array[1] = 2;
        for (int i = 2; i < target; i++) {
            array[i] = array[i - 1] + array[i - 2];
        }
        return array[target - 1];
    }

T11. 二進(jìn)制中 1 的個數(shù)

題目描述

輸入一個整數(shù),輸出該數(shù)二進(jìn)制表示中1的個數(shù)。其中負(fù)數(shù)用補(bǔ)碼表示。

解題思路

Integer.bitCount(n):技術(shù)整型的二進(jìn)制表示中1的個數(shù)。。。

    public int NumberOf1(int n) {//(???)
        return Integer.bitCount(n);
    }

正常算法:&按位與。每次將 n 和 n-1 進(jìn)行 & 運(yùn)算,從右往左去掉二進(jìn)制最右邊的一個1。例如:

n:           100100
n - 1:       100011
n & (n-1):   100000

一次運(yùn)算后,n由100100變?yōu)?00000,去除了一個1。所有1去完時,n為0。

    public int NumberOf1(int n) {//正常算法
        int sum = 0;
        
        while(n != 0) {
            sum ++;
            n = n & (n-1);//&按位與
        }
        
        return sum;
    }

T12. 數(shù)值的整數(shù)次方

題目描述

給定一個double類型的浮點數(shù)base和int類型的整數(shù)exponent。求base的exponent次方。

解題思路

由于xn = (x*x)n/2,通過遞歸求解可減小時間復(fù)雜度,每遞歸一次,n 減一半。時間復(fù)雜度O(logn)。

注意:需要小心 n 的正負(fù)、奇偶。

    public double Power(double base, int exponent) {
        if(exponent == 0) return 1;
        if(exponent == 1) return base;
        
        boolean isPositive = true;//正負(fù)次方以此判斷
        if(exponent < 0) {
            exponent = -exponent;
            isPositive = false;
        }
        
        double pow = Power(base * base, exponent / 2);//遞歸,每次遞歸n減小一半,即 (x*x)^(n/2)
        if(exponent % 2 != 0) pow *= base;//奇次冪的話,/2會少算一次,在這補(bǔ)回來
        
        return isPositive ? pow : (1 / pow);//三元表達(dá)式,正次冪為pow,負(fù)次冪為1/pow
    }

T13. 調(diào)整數(shù)組順序使奇數(shù)位于偶數(shù)前面

題目描述

輸入一個整數(shù)數(shù)組,實現(xiàn)一個函數(shù)來調(diào)整該數(shù)組中數(shù)字的順序,使得所有的奇數(shù)位于數(shù)組的前半部分,所有的偶數(shù)位于數(shù)組的后半部分,并保證奇數(shù)和奇數(shù),偶數(shù)和偶數(shù)之間的相對位置不變。

解題思路

先統(tǒng)計奇偶數(shù)的個數(shù)。在所要求的數(shù)組中,奇數(shù)的個數(shù)即為數(shù)組中偶數(shù)應(yīng)該開始儲存的位置。

clone一個數(shù)組,按順序往原數(shù)組里存,奇數(shù)存前面,偶數(shù)存后面。

    public void reOrderArray(int[] array) {
        int oddNum = 0;
        for(int value: array) {
            if(value % 2 == 1) {
                oddNum++;
            }
        }
        
        int[] copyArray = array.clone();//克隆數(shù)組,對原數(shù)組賦值
        int i = 0, j = oddNum;//j為偶數(shù)開始存儲的位置
        
        for(int num : copyArray) {
            if(num % 2 == 1) {
                array[i] = num;
                i ++;
            } else {
                array[j++] = num;
                j ++;
            }
        }
    }

或者,聲明兩個ArrayList存奇數(shù)和偶數(shù),然后合并。

    public void reOrderArray(int[] array) {
        ArrayList<Integer> odd = new ArrayList<>();
        ArrayList<Integer> even = new ArrayList<>();
        
        for(int i = 0; i < array.length; i ++) {
            if(array[i] % 2 == 0) {
                even.add(array[i]);
            } else {
                odd.add(array[i]);
            }
        }
        odd.addAll(even);

        for(int i = 0; i < array.length; i ++) {
            array[i] = odd.get(i);
        }
    }

T14. 鏈表中倒數(shù)第 K 個結(jié)點

題目描述

輸入一個鏈表,輸出該鏈表中倒數(shù)第k個結(jié)點。

解題思路

假設(shè),共 n 個結(jié)點,倒數(shù)第k個結(jié)點即為第 n-k+1 個結(jié)點。

定義兩個指針node1和node2,使他們都指向第一個結(jié)點。到第 n-k+1 個結(jié)點則需要移動 n-k次。

此時,node1移動n次會指向空。

先讓node1移動 k 次,還剩 n-k 次指向空。

這時,node2與node1同步移動,當(dāng)node1指空時,node2移動了 n-k 次,剛好到第 n-k+1 個結(jié)點。

    public ListNode FindKthToTail(ListNode head, int k) {
        if(head == null) return null;
        
        ListNode node1 = head;
        ListNode node2 = head;
        
        while(node1 != null && k > 0) {//node1移動k次,還有n-k次會指空
            node1 = node1.next;
            k --;
        }
        
        if(k > 0) return null;
        
        while(node1 != null) {//移動n-k次,此時node2到n-k+1個結(jié)點,即倒數(shù)第k個結(jié)點
            node1 = node1.next;
            node2 = node2.next;
        }
        
        return node2;
    }

T15. 反轉(zhuǎn)鏈表

題目描述

輸入一個鏈表,反轉(zhuǎn)鏈表后,輸出新鏈表的表頭。

解題思路

關(guān)鍵在于聲明一個結(jié)點preNode用來記錄前一個結(jié)點,使下一次循環(huán)時的結(jié)點的next指向它,反轉(zhuǎn)順序。

    public ListNode ReverseList(ListNode head) {
        if(head == null || head.next == null) return head;
        
        ListNode preNode = null;
        
        while(head != null) {
            ListNode tmp = head.next;//tmp記錄【下一個結(jié)點】
            head.next = preNode;//【當(dāng)前結(jié)點】的next指向【前一個結(jié)點】,反轉(zhuǎn)鏈表順序
            preNode = head;//preNode記錄【當(dāng)前結(jié)點】,即【下一個結(jié)點】的【前一個結(jié)點】
            head = tmp;//將【當(dāng)前結(jié)點】更改為【下一個結(jié)點】,進(jìn)入下一次循環(huán)
        }
        //當(dāng)head為null時跳出了循環(huán),此時它的前一個結(jié)點preNode即為原鏈表的最后一個結(jié)點,鏈表順序已反轉(zhuǎn)
        return preNode;
    }

T16. 合并兩個排序的鏈表

題目描述

輸入兩個單調(diào)遞增的鏈表,輸出兩個鏈表合成后的鏈表,當(dāng)然我們需要合成后的鏈表滿足單調(diào)不減規(guī)則。

解題思路

聲明一個新鏈表,不斷比較原來兩個鏈表的 val 值,小的插入新鏈表即可。

注意:要求返回的表頭是聲明的鏈表的next結(jié)點。

    public ListNode Merge(ListNode list1, ListNode list2) {
        ListNode head = new ListNode(-9999);
        ListNode tmp = head;
        
        while(list1 != null && list2 != null) {
            if(list1.val < list2.val) {//比較兩個鏈表當(dāng)前結(jié)點的值,小的先插入新鏈表
                tmp.next = list1;
                list1 = list1.next;
            } else {
                tmp.next = list2;
                list2 = list2.next;
            }
            tmp = tmp.next;
        }
        
        //容錯,將剩余鏈表補(bǔ)到新鏈表結(jié)尾,此時能保持單調(diào)不減
        if(list1 != null) tmp.next = list1;
        if(list2 != null) tmp.next = list2;
        
        return head.next;//記得head為聲明的無意義表頭,head.next才是新鏈表的頭
    }

項目地址https://github.com/JohnnyJYWu/offer-Java

下一篇算法 | 一周刷完《劍指Offer》 Day2:第17~26題

希望這篇文章對你有幫助~

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

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