算法 | 一週刷完《劍指Offer》 Day3:第27~37題

寫在前面


Day3:第27~37題

難度上升,多看多想,理解纔好做。

  • T27. 字符串的排列
  • T28. 數組中出現次數超過一半的數字
  • T29. 最小的K個數
  • T30. 連續子數組的最大和
  • T31. 整數中1出現的次數(從1到n整數中1出現的次數)
  • T32. 把數組排成最小的數
  • T33. 醜數
  • T34. 第一個只出現一次的字符位置
  • T35. 數組中的逆序對
  • T36. 兩個鏈表的第一個公共結點
  • T37. 數字在排序數組中出現的次數

T27. 字符串的排列

題目描述

輸入一個字符串,按字典序打印出該字符串中字符的所有排列。例如輸入字符串abc,則打印出由字符a,b,c所能排列出來的所有字符串abc,acb,bac,bca,cab和cba。

輸入描述:輸入一個字符串,長度不超過9(可能有字符重複),字符只包括大小寫字母。

解題思路

同樣是遞歸回溯的思想。先把字符串進行字典序排序,定義hasUsed輔助數組記錄各字符是否使用,然後遞歸對後面的字符排列組合即可。

注意:使用StringBuffer便於字符串操作。每個遞歸結束後記得回溯,去除此循環加入的字符,回退到上一步的排列,與T24中去除節點道理一樣。

	private ArrayList<String> result = new ArrayList<>();
	
	public ArrayList<String> Permutation(String str) {
		if(str == null || str.length() == 0) return result;
		
		char[] chars = str.toCharArray();
		Arrays.sort(chars);//字典序排序
		
		permutation(chars,
				new boolean[chars.length],//用於記錄當前字符是否用過
				new StringBuffer());//字符串,便於操作
		
		return result;
    }
	
	private void permutation(char[] chars, boolean[] hasUsed, StringBuffer str) {
		if(str.length() == chars.length) {//長度相同說明出結果,加入result
			result.add(str.toString());
			return;
		}
		
		for(int i = 0; i < chars.length; i++) {
			if(hasUsed[i]) continue;
	        if(i != 0 && chars[i] == chars[i - 1] && !hasUsed[i - 1]) continue;//連續兩個值相同時,保證不重複
	        hasUsed[i] = true;
	        str.append(chars[i]);
	        
	        //遞歸對後面的字符進行排列
	        permutation(chars, hasUsed, str);
	        
	        //此步重要,去除此循環加入的字符,回退到上一步的排列,與T24中去除節點道理一樣
	        str.deleteCharAt(str.length() - 1);
	        hasUsed[i] = false;
	        
		}
	}

T28. 數組中出現次數超過一半的數字

題目描述

數組中有一個數字出現的次數超過數組長度的一半,請找出這個數字。例如輸入一個長度爲9的數組{1,2,3,2,2,2,5,4,2}。由於數字2在數組中出現了5次,超過數組長度的一半,因此輸出2。如果不存在則輸出0。

解題思路

多數投票問題。

首先明確,若數字出現次數超過一半,那它必爲出現最多的數字。因此問題轉換爲找出現最多的數字,然後判斷它出現的次數是否超過一半。

定義count來統計一個元素出現的次數,當遍歷到的元素和統計元素不相等時,count --。如果前面查找了 i 個元素,且 count == 0 ,說明前 i 個元素沒有【多數】,或者有【多數】但出現的次數少於 i / 2 ,因爲如果多於 i / 2 的話 count 就一定不會爲 0 。此時剩下的 n - i 個元素中,【多數】的數目依然多於 (n - i) / 2,因此繼續查找就能找出【多數】。

最後,找到多數後再判斷出現次數是否超過一半即可。

	public int MoreThanHalfNum_Solution(int[] array) {//多數投票問題
        int num = array[0];
        int count = 1;
        
        for(int i = 1; i < array.length; i ++) {
        	if(array[i] == num) {
        		count ++;
        	} else {
        		count --;
        	}
    		if(count == 0) {
    			num = array[i];
    			count = 1;
    		}
        }
        
        count = 0;
        for(int val: array) {
        	if(val == num) {
        		count ++;
        	}
        }
        
        return count > array.length / 2 ? num : 0;//三元
    }

T29. 最小的K個數

題目描述

輸入n個整數,找出其中最小的K個數。例如輸入4,5,1,6,2,7,3,8這8個數字,則最小的4個數字是1,2,3,4,。

解題思路

快速選擇法。快速選擇的總體思路與快速排序一致,選擇一個元素作爲基準來對元素進行分區,將小於和大於基準的元素分在基準左邊和右邊的兩個區域。不同的是,快速選擇並不遞歸訪問雙邊,而是隻遞歸進入一邊的元素中繼續尋找。

快排的 partition() 方法會返回一個整數 j 使得 a[l…j-1] 小於等於 a[j],且 a[j+1…h] 大於等於 a[j],此時 a[j] 就是數組的第 j 大元素。可以利用這個特性找出數組的第 K 個元素。

	public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
		ArrayList<Integer> list = new ArrayList<>();
        
        if(k > input.length || k <= 0) return list;
		
        int smallestK = findSmallestK(input, k - 1);
        
        for(int val: input) {
        	if(val <= smallestK && list.size() < k) {
        		list.add(val);
        	}
        }
		return list;
    }
	
	private int findSmallestK(int[] input, int k) {
	    int low = 0;
	    int high = input.length - 1;
	    while(low < high) {
	        int j = partition(input, low, high);
	        if(j < k) {
	            low = j + 1;
	        } else if(j > k) {
	            high = j - 1;
	        } else {
	            break;
	        }
	    }
	    return input[k];
	}
	
	private int partition(int[] nums, int low, int high) {
	    int i = low;
	    int j = high + 1;
	    while(true) {
	        while(i < high && nums[++ i] < nums[low]) ;
	        while(j > low && nums[low] < nums[-- j]) ;
	        if(i >= j) {
	            break;
	        }
	        swap(nums, i, j);
	    }
	    swap(nums, low, j);
	    return j;
	}
	
	private void swap(int[] nums, int i, int j) {
	    int tmp = nums[i];
	    nums[i] = nums[j];
	    nums[j] = tmp;
	}

或者,直接排序找最小。。。

	public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
		ArrayList<Integer> list = new ArrayList<>();
        
        if(k > input.length || k <= 0) return list;
		
		Arrays.sort(input);
		for(int i = 0; i < k; i ++) {
			list.add(input[i]);
		}
		
		return list;
    }

T30. 連續子數組的最大和

題目描述

HZ偶爾會拿些專業問題來忽悠那些非計算機專業的同學。今天測試組開完會後,他又發話了:在古老的一維模式識別中,常常需要計算連續子向量的最大和,當向量全爲正數的時候,問題很好解決。但是,如果向量中包含負數,是否應該包含某個負數,並期望旁邊的正數會彌補它呢?例如:{6,-3,-2,7,-15,1,2,2},連續子向量的最大和爲8(從第0個開始,到第3個爲止)。給一個數組,返回它的最大連續子序列的和,你會不會被他忽悠住?(子向量的長度至少是1)

解題思路

嗯。。。閱讀題,歷來都是題目越長題越簡單。。。單純一點,邊找邊加就行了。

注意查看代碼中唯一的一行註釋,很關鍵。

	public int FindGreatestSumOfSubArray(int[] array) {
        if(array == null || array.length == 0) return 0;
        
        int sum = 0;
        int result = Integer.MIN_VALUE;
        
        for(int val: array) {
        	if(sum < 0) {
        		sum = val;//關鍵在此,如果前面n個的和sum已經小於0了,別傻乎乎繼續加,直接從新的val開始吧
        	} else {
        		sum += val;
        	}
        	
        	if(result < sum) {
        		result = sum;
        	}
        }
        
        return result;
    }

T31. 整數中1出現的次數(從1到n整數中1出現的次數)

題目描述

求出1 ~ 13的整數中1出現的次數,並算出100 ~ 1300?的整數中1出現的次數?爲此他特別數了一下1 ~ 13中包含1的數字有1、10、11、12、13因此共出現6次,但是對於後面問題他就沒轍了。ACMer希望你們幫幫他,並把問題更加普遍化,可以很快的求出任意非負整數區間中1出現的次數(從1 到 n 中1出現的次數)。

解題思路

這種題靠悟性。。。

	public int NumberOf1Between1AndN_Solution(int n) {
		int ones = 0;
		
        for(int m = 1; m <= n; m *= 10) {
            int a = n / m, b = n % m;
            if(a % 10 == 0)
                ones += a / 10 * m;
            else if(a % 10 == 1) 
                ones += (a / 10 * m) + (b + 1);
            else
                ones += (a / 10 + 1) * m;
        }
        
        return ones;
    }

leetcode大神只用了5行的解法,有興趣的深入瞭解一下。。。
https://leetcode.com/problems/number-of-digit-one/discuss/64381/4-lines-olog-n-cjavapython

	public int countDigitOne(int n) {
	    int ones = 0;
	    for (long m = 1; m <= n; m *= 10)
	        ones += (n/m + 8) / 10 * m + (n/m % 10 == 1 ? n%m + 1 : 0);
	    return ones;
	}

T32. 把數組排成最小的數

題目描述

輸入一個正整數數組,把數組裏所有數字拼接起來排成一個數,打印能拼接出的所有數字中最小的一個。例如輸入數組{3,32,321},則打印出這三個數字能排成的最小數字爲321323。

解題思路

可以看做是排序問題,不同點在於此題是比較數字轉換成字符串後相加的大小

例如兩個數字轉換的字符串S1和S2,應該比較 S1+S2S2+S1 的大小,如果 S1+S2 < S2+S1,那麼應該把 S1 排在前面,否則應該把 S2 排在前面。

	public String PrintMinNumber(int[] numbers) {
		String[] nums = new String[numbers.length];
		for(int i = 0; i < nums.length; i ++) {//int轉string,比較string相加的值
			nums[i] = String.valueOf(numbers[i]);
		}
		
		Arrays.sort(nums, (s1, s2) -> (s1 + s2).compareTo(s2 + s1));//排序,s1+s2與s2+s1兩個字符串比較,誰小誰放前面
		String result = "";
		for(String str: nums) {
			result += str;
		}
		
		return result;
	}

T33. 醜數

題目描述

把只包含質因子2、3和5的數稱作醜數(Ugly Number)。例如6、8都是醜數,但14不是,因爲它包含質因子7。 習慣上我們把1當做是第一個醜數。求按從小到大的順序的第N個醜數。

解題思路

此題需要思維靈活。由題意,只需不斷從前面已知的醜數中選取合適的醜數分別乘2、3、5,選取最小的醜數加入數組即可。關鍵在於如何選取合適的醜數。(定義2、3、5對應的下標index2、index3、index5,詳見註釋)

	public int GetUglyNumber_Solution(int index) {
		if(index <= 6) return index;//1~6即爲前6個醜數
		
		int index2 = 0, index3 = 0, index5 = 0;
		
		int[] uglys = new int[index];//存前n個醜數
		uglys[0] = 1;//初始化第一個值爲1
		int n = 1;//開始計算第二個醜數
		
		while(n < index) {
			//找出下一個小的醜數,此步重要需理解,分別用2,3,5在醜數數組裏對應的上一個醜數乘2,3,5找出最小的醜數
			int ugly2 = uglys[index2] * 2;
			int ugly3 = uglys[index3] * 3;
			int ugly5 = uglys[index5] * 5;
			int min = Math.min(ugly2, Math.min(ugly3, ugly5));
			
			uglys[n] = min;
			n ++;
			
			//將2,3,5對應的上一個醜數後移
			if(min == ugly2) index2 ++;
			if(min == ugly3) index3 ++;
			if(min == ugly5) index5 ++;
		}
		
        return uglys[index - 1];
    }

T34. 第一個只出現一次的字符位置

題目描述

在一個字符串(0<=字符串長度<=10000,全部由字母組成)中找到第一個只出現一次的字符,並返回它的位置,如果沒有則返回 -1(需要區分大小寫)。

解題思路

char類型一般爲一個字節,範圍在0 ~ 255。因此定義一個整形計數數組int[256],對每個char出現次數進行計數即可。

計數後要按照字符串中的字符順序查找第一個計數次數爲1的字符。

	public int FirstNotRepeatingChar(String str) {
        int[] array = new int[256];//計數數組
        
        for(int i = 0; i < str.length(); i ++) {
        	array[str.charAt(i)] ++;
        }
        
        for(int i = 0; i < str.length(); i ++) {
        	if(array[str.charAt(i)] == 1) {//按str的字符順序來,找出第一個計數次數爲1的即爲所求位置
        		return i;
        	}
        }
        
        return -1;
    }

T35. 數組中的逆序對

題目描述

在數組中的兩個數字,如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個數組,求出這個數組中的逆序對的總數P。並將P對1000000007取模的結果輸出。 即輸出P%1000000007。

解題思路

分治思想,先分後治。先不斷將數組一分爲二,並對這分開的兩部分進行相同操作;然後一邊合併相鄰的子數組,一邊統計逆序對的數目。(實質就是歸併排序的思路)

	private long cnt = 0;
	private int[] tmp;//輔助數組
	
	public int InversePairs(int[] array) {
		tmp = new int[array.length];
		
	    mergeSortUp2Down(array, 0, array.length - 1);
        
	    return (int) (cnt % 1000000007);
    }
	
	private void mergeSortUp2Down(int[] nums, int first, int last) {
		if(last - first < 1) return;
		
		int mid = (first + last) / 2;
		//分治思想
		mergeSortUp2Down(nums, first, mid);
		mergeSortUp2Down(nums, mid + 1, last);
		merge(nums, first, mid ,last);
	}
	
	private void merge(int[] nums, int first, int mid, int last) {
		int i = first, j = mid + 1, k = first;
		
	    while(i <= mid || j <= last) {
	        if(i > mid) {
	        	tmp[k] = nums[j];
	        	j ++;
	        }
	        else if(j > last) {
	        	tmp[k] = nums[i];
	        	i ++;
	        }
	        else if(nums[i] < nums[j]) {
	        	tmp[k] = nums[i];
	        	i ++;
	        }
	        else {
	            tmp[k] = nums[j];
	            j ++;
	            this.cnt += mid - i + 1;//nums[i] > nums[j]說明nums[i...mid]都大於nums[j]
	        }
	        k ++;
	    }
	    for(k = first; k <= last; k ++) {
	        nums[k] = tmp[k];
	    }
	}

T36. 兩個鏈表的第一個公共結點

題目描述

輸入兩個鏈表,找出它們的第一個公共結點。

解題思路


數學問題。

如圖,鏈表1長度爲 a+c,鏈表2長度爲 b+c。聲明兩個指針node1和node2分別指向兩個鏈表表頭,同步向後移動。

node1走過 a+c 後指空,此時讓它指向鏈表2的表頭並繼續向後走;同理node2走過 b+c 後指向鏈表1表頭。

由於 a+c+b = b+c+a ,此時node1和node2剛好相遇,且相遇在兩個鏈表的第一個公共節點。由此得解。

	public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
		ListNode node1 = pHead1;
		ListNode node2 = pHead2;
		
		while(node1 != node2) {//公共節點後面即爲公共鏈表
			if(node1 == null) {
				node1 = pHead2;
			} else {
				node1 = node1.next;
			}
			
			if(node2 == null) {
				node2 = pHead1;
			} else {
				node2 = node2.next;
			}
		}
		
		return node1;
    }

T37. 數字在排序數組中出現的次數

題目描述

統計一個數字在排序數組中出現的次數。

解題思路

順序查找。

	public int GetNumberOfK(int[] array , int k) {
		int sum = 0;
		
		for(int val: array) {
			if(val == k) {
				sum ++;
			}
		}
		
		return sum;
    }

二分查找,找到第一次和最後一次k出現的位置,即可計算次數。

	public int GetNumberOfK(int[] array , int k) {
		int first = getFirstK(array, k);
		int last = getLastK(array, k);
		
		if(first == -1) return 0;
		if(last == -1) return 0;
		
		return last - first + 1;
    }
	
	private int getFirstK(int[] array , int k) {
		int low = 0, high = array.length - 1;
		
	    while (low <= high) {
	        int mid = (high + low) / 2;
	        if(array[mid] >= k) {
	        	high = mid - 1;
	        } else {
	        	low = mid + 1;
	        }
	    }
	    
	    if(low > array.length - 1 || array[low] != k) 
	    	return -1;
	    
	    return low;
	}
	
	private int getLastK(int[] array , int k) {
		int low = 0, high = array.length - 1;
		
	    while (low <= high) {
	        int mid = (high + low) / 2;
	        if(array[mid] > k) {
	        	high = mid - 1;
	        } else {
	        	low = mid + 1;
	        }
	    }
	    
	    if(high < 0 || array[high] != k) 
	    	return -1;
	    
	    return high;
	}

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

上一篇算法 | 一週刷完《劍指Offer》 Day2:第17~26題
下一篇:算法 | 一週刷完《劍指Offer》 Day4:第38~50題

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

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