算法 | 一周刷完《剑指Offer》 Day3:第27~37题

写在前面

  • 本系列包含《剑指Offer》66道算法题,预计一周刷完,这是第三篇。
    系列汇总:剑指Offer 66题 Java 刷题笔记汇总
  • 所有题目均可在牛客网在线编程平台进行调试。
    网址:https://www.nowcoder.com/ta/coding-interviews
  • 本系列包含题目,解题思路代码(Java)
    代码同步发布在GitHub:https://github.com/JohnnyJYWu/offer-Java

上一篇:算法 | 一周刷完《剑指Offer》 Day2:第17~26题
下一篇:算法 | 一周刷完《剑指Offer》 Day4:第38~49题


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. 两个链表的第一个公共结点

题目描述

输入两个链表,找出它们的第一个公共结点。

解题思路

算法 | 一周刷完《剑指Offer》 Day3:第27~37题_第1张图片
数学问题。

如图,链表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~49题

希望这篇文章对你有帮助~

你可能感兴趣的:(剑指Offer)