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

開個新坑,準備校招研發崗面試,基本的算法還是要過關的。


寫在前面


Day1:第1~16題

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

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

T1. 二維數組中的查找

題目描述

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

解題思路

因爲矩陣中的每一個數,左邊都比它小,下邊都比它大。因此,從右上角開始查找,就可以根據 target 和當前元素的大小關係來縮小查找區間。

	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. 替換空格

題目描述

請實現一個函數,將一個字符串中的每個空格替換成“%20”。例如,當字符串爲We Are Happy,則經過替換之後的字符串爲We%20Are%20Happy。

解題思路

由於每個空格替換成了三個字符,首先要擴容字符串長度至替換後的長度,因此當遍歷到一個空格時,需要在尾部填充兩個任意字符。
然後聲明兩個下標,一個爲原字符串末尾下標 i,一個爲現字符串末尾 j,兩個下標同步從後往前掃。當 i 指向空格時,j 就倒着依次添加 ‘0’, ‘2’, ‘%’。
這樣做的目的是: j 下標不會超過 i 下標,不會影響原字符串的內容。

	public String replaceSpace(StringBuffer str) {
		int oldLen = str.length();
		
		for(int i = 0; i < oldLen; i ++) {
			if(str.charAt(i) == ' ') {//每出現一個空格,在結尾添加兩個任意字符,擴充字符串長度
				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 == ' ') {//每出現一個空格,替換爲%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。

解題思路

使用棧實現:先入後出。全部入棧,依次出棧,順序即爲從尾到頭。

	public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
		//使用棧實現,先入後出
		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;
	}

使用遞歸實現:先加入鏈表後面的節點,再加入當前節點,順序即爲從尾到頭。

	public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
		//使用遞歸實現,先加入鏈表後面的節點,再加入當前節點
	    ArrayList<Integer> arrayList = new ArrayList<>();
	    
	    if(listNode != null) {
	        arrayList.addAll(printListFromTailToHead(listNode.next));//先遞歸
	        arrayList.add(listNode.val);//再加入當前
	    }
	    
	    return arrayList;
	}

T4. 重建二叉樹

題目描述

輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。

解題思路

首先清楚以下性質:
前序遍歷:根 -> 左 -> 右,中序遍歷:左 -> 根 -> 右。
因此一顆二叉樹中,前序遍歷第一個節點爲根節點,通過它在中序遍歷中的位置,可以將中序遍歷分爲兩部分,左半部分是該節點的左子樹右半部分是該節點的右子樹
根據這個原理,遞歸執行即可重構出二叉樹。

	private Map<Integer, Integer> map = new HashMap<>();//用於查找中序遍歷數組中,每個值對應的索引
	
	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, //前序遍歷及當前在前序遍歷中的區間
								int[] in, int inLeft, int inRight) { //中序遍歷及當前在前序遍歷中的區間
	    if(preLeft == preRight) { //即根據前序遍歷,當前節點無子節點
	    	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;//該節點左半部分的節點數
	    
	    //遞歸,獲取左子樹及右子樹
	    root.left = getBiTree(pre, preLeft + 1, preLeft + leftTreeSize, //左半部分在前序遍歷中的區間
	    						in, inLeft, inIndex - 1);//左半部分在中序遍歷中的區間
	    root.right = getBiTree(pre, preLeft + 1 + leftTreeSize, preRight, //右半部分在前序遍歷中的區間
	    						in, inIndex + 1, inRight);//右半部分在中序遍歷中的區間
	    
	    return root;
	}

T5. 用兩個棧實現隊列

題目描述

用兩個棧來實現一個隊列,完成隊列的Push和Pop操作。 隊列中的元素爲int類型。

解題思路

隊列:先進先出,:先進後出
例如,假設 1 ~ n 的數字順序,入Stack1,出的順序爲 n ~ 1 。
這時,Stack1出棧進入Stack2,進入Stack2的順序爲n ~ 1,那麼Stack2出的順序爲 1 ~ n 。
相當於隊列 1 ~ 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);//然後將當前要進入隊列元素壓入stack1
        while(!stack1.isEmpty()) {//最後將stack1所有元素壓入stack2
        	stack2.push(stack1.pop());//此時stack2中出棧順序即爲隊列出棧順序,相當於先入先出
        }
    }
    
    public int pop() {
    	return stack2.pop();
    }

T6. 旋轉數組的最小數字

題目描述

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

解題思路

我們可以認爲數組分成了兩部分,前半部分是較大的數,後半部分是較小的數
利用二分查找的思路,觀察取中的數在前半部分還是後半部分,以此來找出最小的數(後半部分的第一個數)。

	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在前半區大的數裏
	        	low = mid;
	        } else {//此時mid在後半區小的數裏
	        	high = mid;
	        }
	    }
		return array[low];
    }

T7. 斐波那契數列

題目描述

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

解題思路

斐波那契數列: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級的臺階總共有多少種跳法(先後次序不同算不同的結果)。

解題思路

動態規劃的思想。這裏採用倒着往前推的方法遞減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) {//遞歸求可能出現的情況總數(最後一次跳1或最後一次跳2,倒着往前推)
			return JumpFloor(target - 1) + JumpFloor(target - 2);
		}
		
		return 0;
    }

或者,其本質上是斐波那契數列的原理。

	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. 變態跳臺階

題目描述

一隻青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個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);//對於本次的跳躍又有多少種跳法?遞歸獲取結果
		}
		
		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. 二進制中 1 的個數

題目描述

輸入一個整數,輸出該數二進制表示中1的個數。其中負數用補碼錶示。

解題思路

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

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

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

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

一次運算後,n由100100變爲100000,去除了一個1。所有1去完時,n爲0。

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

T12. 數值的整數次方

題目描述

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

解題思路

由於xn = (x*x)n/2,通過遞歸求解可減小時間複雜度,每遞歸一次,n 減一半。時間複雜度O(logn)。
注意:需要小心 n 的正負、奇偶。

	public double Power(double base, int exponent) {
		if(exponent == 0) return 1;
	    if(exponent == 1) return base;
	    
	    boolean isPositive = true;//正負次方以此判斷
	    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會少算一次,在這補回來
	    
	    return isPositive ? pow : (1 / pow);//三元表達式,正次冪爲pow,負次冪爲1/pow
    }

T13. 調整數組順序使奇數位於偶數前面

題目描述

輸入一個整數數組,實現一個函數來調整該數組中數字的順序,使得所有的奇數位於數組的前半部分,所有的偶數位於數組的後半部分,並保證奇數和奇數,偶數和偶數之間的相對位置不變。

解題思路

先統計奇偶數的個數。在所要求的數組中,奇數的個數即爲數組中偶數應該開始儲存的位置。
clone一個數組,按順序往原數組裏存,奇數存前面,偶數存後面。

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

或者,聲明兩個ArrayList存奇數和偶數,然後合併。

	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. 鏈表中倒數第 K 個結點

題目描述

輸入一個鏈表,輸出該鏈表中倒數第k個結點。

解題思路

假設,共 n 個節點,倒數第k個結點即爲第 n-k+1 個節點。
定義兩個指針node1和node2,使他們都指向第一個節點。到第 n-k+1 個節點則需要移動 n-k次。
此時,node1移動n次會指向空。
先讓node1移動 k 次,還剩 n-k 次指向空。
這時,node2與node1同步移動,當node1指空時,node2移動了 n-k 次,剛好到第 n-k+1 個節點。

	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個節點,即倒數第k個節點
			node1 = node1.next;
			node2 = node2.next;
		}
		
		return node2;
	}

T15. 反轉鏈表

題目描述

輸入一個鏈表,反轉鏈表後,輸出新鏈表的表頭。

解題思路

關鍵在於聲明一個節點preNode用來記錄前一個節點,使下一次循環時的節點的next指向它,反轉順序。

	public ListNode ReverseList(ListNode head) {
		if(head == null || head.next == null) return head;
		
		ListNode preNode = null;
		
		while(head != null) {
			ListNode tmp = head.next;//tmp記錄【下一個節點】
			head.next = preNode;//【當前節點】的next指向【前一個節點】,反轉鏈表順序
			preNode = head;//preNode記錄【當前節點】,即【下一個節點】的【前一個節點】
			head = tmp;//將【當前節點】更改爲【下一個節點】,進入下一次循環
		}
		//當head爲null時跳出了循環,此時它的前一個節點preNode即爲原鏈表的最後一個節點,鏈表順序已反轉
		return preNode;
    }

T16. 合併兩個排序的鏈表

題目描述

輸入兩個單調遞增的鏈表,輸出兩個鏈表合成後的鏈表,當然我們需要合成後的鏈表滿足單調不減規則。

解題思路

聲明一個新鏈表,不斷比較原來兩個鏈表的 val 值,小的插入新鏈表即可。
注意:要求返回的表頭是聲明的鏈表的next節點。

	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) {//比較兩個鏈表當前節點的值,小的先插入新鏈表
				tmp.next = list1;
				list1 = list1.next;
			} else {
				tmp.next = list2;
				list2 = list2.next;
			}
			tmp = tmp.next;
		}
		
		//容錯,將剩餘鏈表補到新鏈表結尾,此時能保持單調不減
		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題

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

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