首页 文章 精选 留言 我的

精选列表

搜索[轻量版],共10000篇文章
优秀的个人博客,低调大师

LeetCode刷题指南(Java版)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a724888/article/details/81389292 微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站。(关注公众号后回复”Java“即可领取 Java基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的Java学习指南、Java程序员面试指南等干货资源) ​ 参考@CyC2018的leetcode题解。Java工程师LeetCode刷题必备。主要根据LeetCode的tag进行模块划分,每部分都选取了比较经典的题目,题目以medium和easy为主,少量hard题目。 我在@CyC2018大佬的基础上又加上了部分题解,并且筛选了比较经典的题目,去除了比较晦涩难懂的题目,以及一些很少考的题目,以便大家积累经验,在面试笔试中能够游刃有余。 详情请见我的仓库:https://github.com/h2pl/leetcode 数组和矩阵 把数组中的 0 移到末尾 283. Move Zeroes (Easy) For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0]. public void moveZeroes(int[] nums) { int idx = 0; for (int num : nums) { if (num != 0) { nums[idx++] = num; } } while (idx 字符串 两个字符串包含的字符是否完全相同 242. Valid Anagram (Easy) s = "anagram", t = "nagaram", return true. s = "rat", t = "car", return false. 字符串只包含小写字符,总共有 26 个小写字符。可以用 HashMap 来映射字符与出现次数。因为键的范围很小,因此可以使用长度为 26 的整型数组对字符串出现的字符进行统计,然后比较两个字符串出现的字符数量是否相同。 public boolean isAnagram(String s, String t) { int[] cnts = new int[26]; for (char c : s.toCharArray()) { cnts[c - 'a']++; } for (char c : t.toCharArray()) { cnts[c - 'a']--; } for (int cnt : cnts) { if (cnt != 0) { return false; } } return true; } 计算一组字符集合可以组成的回文字符串的最大长度 409. Longest Palindrome (Easy) Input : "abccccdd" Output : 7 Explanation : One longest palindrome that can be built is "dccaccd", whose length is 7. 使用长度为 256 的整型数组来统计每个字符出现的个数,每个字符有偶数个可以用来构成回文字符串。 因为回文字符串最中间的那个字符可以单独出现,所以如果有单独的字符就把它放到最中间。 public int longestPalindrome(String s) { int[] cnts = new int[256]; for (char c : s.toCharArray()) { cnts[c]++; } int palindrome = 0; for (int cnt : cnts) { palindrome += (cnt / 2) * 2; } if (palindrome 栈和队列 哈希表 哈希表使用 O(N) 空间复杂度存储数据,从而能够以 O(1) 时间复杂度求解问题。 Java 中的 HashSet 用于存储一个集合,可以查找元素是否在集合中。 如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在。例如对于只有小写字符的元素,就可以用一个长度为 26 的布尔数组来存储一个字符集合,使得空间复杂度降低为 O(1)。 Java 中的 HashMap 主要用于映射关系,从而把两个元素联系起来。 在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中Leetcdoe : 535. Encode and Decode TinyURL (Medium),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。 HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。 数组中的两个数和为给定值 1. Two Sum (Easy) 可以先对数组进行排序,然后使用双指针方法或者二分查找方法。这样做的时间复杂度为 O(NlogN),空间复杂度为 O(1)。 用 HashMap 存储数组元素和索引的映射,在访问到 nums[i] 时,判断 HashMap 中是否存在 target - nums[i],如果存在说明 target - nums[i] 所在的索引和 i 就是要找的两个数。该方法的时间复杂度为 O(N),空间复杂度为 O(N),使用空间来换取时间。 public int[] twoSum(int[] nums, int target) { HashMap 贪心算法 一般什么时候需要用到贪心,其实就是在题目推导比较难解,但是直观思维却比较简单。比如经典的排课问题,就是使用贪心,先进行排序,再进行选择,贪心算法也时常用来求近似解。 所以一般解法可以考虑为,先排序,再根据条件求结果。证明的过程是非常难的,所以我们一般不会讨论证明 贪心思想 贪心思想保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。 455.分发饼干: 假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。 注意: 你可以假设胃口值为正。 一个小朋友最多只能拥有一块饼干。 示例 1: 输入: [1,2,3], [1,1] 输出: 1 解释: 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。 虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 所以你应该输出1。 示例 2: 输入: [1,2], [1,2,3] 输出: 2 解释: 你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。 你拥有的饼干数量和尺寸都足以让所有孩子满足。 所以你应该输出2. 由于题目想让尽量多的孩子满足胃口值,所以应该先用量小的饼干满足胃口小的。这样得到的结果是最优的。 public int findContentChildren(int[] g, int[] s) { int count = 0; Arrays.sort(g); Arrays.sort(s); int i = 0,j = 0; while (i < g.length && j < s.length) { if (g[i] <= s[j]) { i ++; j ++; count ++; }else { j ++; } } return count; } 无重叠区间:给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。 注意: 可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。 示例 1: 输入: [ [1,2], [2,3], [3,4], [1,3] ] 输出: 1 解释: 移除 [1,3] 后,剩下的区间没有重叠。 示例 2: 输入: [ [1,2], [1,2], [1,2] ] 输出: 2 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。 示例 3: 输入: [ [1,2], [2,3] ] 输出: 0 解释: 你不需要移除任何区间,因为它们已经是无重叠的了。 本题类似于课程排课,我们应该让课程结束时间最早的先排课,这样可以让排课最大化,并且需要让课程结束的时间小于下一节课程开始的时间。并且[1,2][2,3]不算课程重叠。 所以我们的想法是,根据数组的第二位进行排序,也就是按照课程的结束时间排序,然后依次寻找不重叠的区间,然后用总个数减去不重叠的区间,剩下的就是要删除的区间。 不过,要注意的是,不重叠的区间并不一定是连续的,如果1和2区间重叠了,还要判断1和3是否重叠,直到找到不重叠的区间,再从3区间开始找下一个区间。 /** * Definition for an interval. * public class Interval { * int start; * int end; * Interval() { start = 0; end = 0; } * Interval(int s, int e) { start = s; end = e; } * } */ import java.util.*; class Solution { public int eraseOverlapIntervals(Interval[] intervals) { int len = intervals.length; if (len <= 1)return 0; Arrays.sort(intervals, (a,b) -> a.end - b.end); int count = 1; int end = intervals[0].end; for (int i = 1;i < intervals.length;i ++) { if (intervals[i].start < end) { continue; } count ++; end = intervals[i].end; } return len - count; } } 本题要注意的点有几个: 1 需要用一个值标识起始值的end,然后再往后找一个符合条件的end。由于是顺序查找,所以只需要一个变量i。并且使用end标识起始元素。 2 默认的count应该为1,因为自己本身就是不重叠的。所以找到其他不重叠的区域,使用n-count才对。 用最少数量的箭引爆气球 在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在104个气球。 一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。 Example: 输入: [[10,16], [2,8], [1,6], [7,12]] 输出: 2 解释: 对于该样例,我们可以在x = 6(射爆[2,8],[1,6]两个气球)和 x = 11(射爆另外两个气球)。 import java.util.*; class Solution { public int findMinArrowShots(int[][] points) { if (points.length <= 1){ return points.length; } Arrays.sort(points, (a, b) -> a[1] - b[1]); int end = points[0][1]; int cnt = 1; for (int i = 1;i < points.length;i ++) { if (points[i][0] <= end) { continue; } end = points[i][1]; cnt ++; } return cnt; } } 和上一题类似,要注意的地方是: 1.本题是求不重叠区域的个数,而上一题是求要删除重叠区域的个数。 2.本题中[1,2][2,3]也算是重叠区域 根据身高重建队列 这题思路不直观,跳过 划分字母区间 字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一个字母只会出现在其中的一个片段。返回一个表示每个字符串片段的长度的列表。 示例 1: 输入: S = “ababcbacadefegdehijhklij” 输出: [9,7,8] 解释: 划分结果为 “ababcbaca”, “defegde”, “hijhklij”。 每个字母最多出现在一个片段中。 像 “ababcbacadefegde”, “hijhklij” 的划分是错误的,因为划分的片段数较少。 注意: S的长度在[1, 500]之间。 S只包含小写字母’a’到’z’。 本题的思路是,先把每个字母的最后一位找出来,存在数组里,然后从头开始找到这样一个字符串,对于字符串中的每个字母,它出现的最后一个字母已经包含在整个字符串内。 import java.util.*; class Solution { public List<Integer> partitionLabels(String S) { int []arr = new int[26]; List<Integer> list = new ArrayList<>(); for (int i = 0;i < S.length();i ++) { arr[S.charAt(i) - 'a'] = i; } int start = 0; int end = arr[S.charAt(0) - 'a']; for (int i = 0;i < S.length();i ++) { end = Math.max(arr[S.charAt(i) - 'a'], end); if (i < end) { continue; }else { list.add(end - start + 1); start = i + 1; } } return list; } } 本题要点: 1.要使用一个数组存储每个字母的最后出现位置。 通过x - ‘a’的方式得到其下标。 2.由于需要每一次截取的长度,所以用start和end来表示,可以用于保存长度。 种花问题 假设你有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花卉不能种植在相邻的地块上,它们会争夺水源,两者都会死去。 给定一个花坛(表示为一个数组包含0和1,其中0表示没种植花,1表示种植了花),和一个数 n 。能否在不打破种植规则的情况下种入 n 朵花?能则返回True,不能则返回False。 示例 1: 输入: flowerbed = [1,0,0,0,1], n = 1 输出: True 示例 2: 输入: flowerbed = [1,0,0,0,1], n = 2 输出: False 注意: 数组内已种好的花不会违反种植规则。 输入的数组长度范围为 [1, 20000]。 n 是非负整数,且不会超过输入数组的大小。 思路:算出花坛中一共有几个空位,看看是否大于等于花的数量 class Solution { public boolean canPlaceFlowers(int[] flowerbed, int n) { int cnt = 0; if (flowerbed.length == 1 && flowerbed[0] == 0) { return n <= 1; } if (flowerbed.length >= 2) { if (flowerbed[0] == 0 && flowerbed[1] == 0) { flowerbed[0] = 1; cnt ++; } if (flowerbed[flowerbed.length - 1] == 0 && flowerbed[flowerbed.length - 2] == 0) { flowerbed[flowerbed.length - 1] = 1; cnt ++; } } for (int i = 1;i < flowerbed.length - 1;) { if (flowerbed[i - 1] == 0 && flowerbed[i] == 0 && flowerbed[i + 1] == 0 ) { cnt ++; flowerbed[i] = 1; i = i + 2; }else { i ++; } } return cnt >= n; } } 注意点: 1从头到尾找到符合0 0 0情况的个数。 2注意数组两边的特殊情况处理 0 0。当长度大于1时处理即可。 3。处理长度为1时的数组 判断子序列 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。 字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,”ace”是”abcde”的一个子序列,而”aec”不是)。 示例 1: s = “abc”, t = “ahbgdc” 返回 true. 示例 2: s = “axc”, t = “ahbgdc” 返回 false. 解析:本题我刚开始想的办法是使用dp求出LCS最长公共子序列,判断长度是否等于t的长度,结果超时了。事实证明我想太多了。 只需要按顺序查找t的字母是否都在s中即可,当然,要注意查找时候的下标移动,否则也是O(N2)的复杂度 DP解法:超时 import java.util.*; class Solution { public boolean isSubsequence(String s, String t) { return LCS(s,t); } public boolean LCS(String s, String t) { int [][]dp = new int[s.length() + 1][t.length() + 1]; for (int i = 1;i <= s.length();i ++) { for (int j = 1;j <= t.length();j ++) { if (s.charAt(i - 1) == t.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1] + 1; }else { dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } } int len = dp[s.length()][t.length()]; return len == s.length(); } } 正解: 巧用indexOf方法indexOf(c,index + 1)来找到从index + 1开始的c字母。 import java.util.*; class Solution { public boolean isSubsequence(String s, String t) { int index = -1; for (int i = 0;i < s.length();i ++) { index = t.indexOf(s.charAt(i), index + 1); if (index == -1) { return false; } } return true; } } 非递减数列 这题暂时没有想到比较好的方法 给定一个长度为 n 的整数数组,你的任务是判断在最多改变 1 个元素的情况下,该数组能否变成一个非递减数列。 我们是这样定义一个非递减数列的: 对于数组中所有的 i (1 <= i < n),满足 array[i] <= array[i + 1]。 示例 1: 输入: [4,2,3] 输出: True 解释: 你可以通过把第一个4变成1来使得它成为一个非递减数列。 示例 2: 输入: [4,2,1] 输出: False 解释: 你不能在只改变一个元素的情况下将其变为非递减数列。 买卖股票的最佳时机 II 题意: 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 示例 1: 输入: [7,1,5,3,6,4] 输出: 7 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。 示例 2: 输入: [1,2,3,4,5] 输出: 4 解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。 因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 示例 3: 输入: [7,6,4,3,1] 输出: 0 解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 题意:只要出现价差为正时就买入,这样一定是最赚的,注意本题中同一天可以进行卖出后再进行买入。 对于 [a, b, c, d],如果有 a 双指针 双指针 双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。 双指针其实一般不会抽取出来单独作为一种算法,因为数组中经常会用到,而且我们熟悉的二分查找也使用了双指针。 二分查找 两数之和 II - 输入有序数组 给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。 函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。 说明: 返回的下标值(index1 和 index2)不是从零开始的。 你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。 示例: 输入: numbers = [2, 7, 11, 15], target = 9 输出: [1,2] 解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。 这题基本操作了。 class Solution { public int[] twoSum(int[] numbers, int target) { int left = 0,right = numbers.length - 1; int []arr = new int[2]; while (left < right) { if (numbers[left] + numbers[right] < target) { left ++; }else if (numbers[left] + numbers[right] > target) { right --; }else { arr[0] = left + 1; arr[1] = right + 1; return arr; } } return arr; } } 平方数之和 给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c。 示例1: 输入: 5 输出: True 解释: 1 * 1 + 2 * 2 = 5 示例2: 输入: 3 输出: False 基操 import java.util.*; class Solution { public boolean judgeSquareSum(int c) { double n = Math.sqrt(c); for (double i = 0;i <= n;i ++) { double diff = c - i * i; int j = (int) Math.sqrt(diff); if (j * j == diff) { return true; } } return false; } } 反转字符串中的元音字母 编写一个函数,以字符串作为输入,反转该字符串中的元音字母。 示例 1: 给定 s = “hello”, 返回 “holle”. 示例 2: 给定 s = “leetcode”, 返回 “leotcede”. 注意: 元音字母不包括 “y”. 快排思想进行交换即可 import java.util.*; class Solution { public String reverseVowels(String s) { char[] arr = s.toCharArray(); int left = 0,right = s.length() - 1; while (left < right){ while (left < right && !isVowels(arr[left])) { left ++; } while (left < right && !isVowels(arr[right])) { right --; } char temp = arr[left]; arr[left] = arr[right]; arr[right] = temp; left ++; right --; } return String.valueOf(arr); } public boolean isVowels(char c) { char[]arr = {'a', 'i', 'e', 'u', 'o', 'A', 'I', 'E', 'U', 'O'}; for (int k = 0;k < arr.length;k ++) { if (c == arr[k]) { return true; } } return false; } } 验证回文字符串 Ⅱ 给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。 示例 1: 输入: “aba” 输出: True 示例 2: 输入: “abca” 输出: True 解释: 你可以删除c字符。 注意: 字符串只包含从 a-z 的小写字母。字符串的最大长度是50000。 在验证回文的基础上加上一步,当遇到不符合要求的字符时,再往前走一步即可。当然机会只有一次。 本题可能遇到一个问题,如果直接用while循环写的话,会遇到两种情况,一种是左边加一,一种是右边减一。只要一种情况满足即可。所以我们要另外写一个判断函数,然后用||来表示两种情况即可。 class Solution { public boolean validPalindrome(String s) { int left = 0,right = s.length() - 1; while (left < right) { if (s.charAt(left) == s.charAt(right)) { left ++; right --; }else { return valid(s, left + 1,right) || valid(s, left, right - 1); } } return true; } public boolean valid(String s, int i, int j) { int left = i,right = j; while (left < right) { if (s.charAt(left) == s.charAt(right)) { left ++; right --; } else return false; } return true; } } 合并两个有序数组 这题给的用例有毒,不谈。 环形链表 剑指offer 使用双指针,一个指针每次移动一个节点,一个指针每次移动两个节点,如果存在环,那么这两个指针一定会相遇。 通过删除字母匹配到字典里最长单词 给定一个字符串和一个字符串字典,找到字典里面最长的字符串,该字符串可以通过删除给定字符串的某些字符来得到。如果答案不止一个,返回长度最长且字典顺序最小的字符串。如果答案不存在,则返回空字符串。 示例 1: 输入: s = “abpcplea”, d = [“ale”,”apple”,”monkey”,”plea”] 输出: “apple” 示例 2: 输入: s = “abpcplea”, d = [“a”,”b”,”c”] 输出: “a” 说明: 所有输入的字符串只包含小写字母。 字典的大小不会超过 1000。 所有输入的字符串长度不会超过 1000。 解析:本题的双指针不是指左右指针了,而是分别扫描两个字符串所用的指针。 由于题目要求先按照长度排序再按照字典序排序,于是使用比较器可以实现该逻辑,然后再一一匹配即可。 import java.util.*; class Solution { public String findLongestWord(String s, List<String> d) { Collections.sort(d, new Comparator<String>() { @Override public int compare(String o1, String o2) { if (o1.length() != o2.length()) { return o2.length() - o1.length(); } else { return o1.compareTo(o2); } } }); for (String str : d) { int i = 0,j = 0; while (i < s.length() && j < str.length()) { if (s.charAt(i) == str.charAt(j)) { i ++; j ++; }else { i ++; } } if (j == str.length()) { return str; } } return ""; } } 排序 排序 快速选择 一般用于求解 Kth Element 问题,可以在 O(N) 时间复杂度,O(1) 空间复杂度完成求解工作。 与快速排序一样,快速选择一般需要先打乱数组,否则最坏情况下时间复杂度为 O(N2)。 堆排序 堆排序用于求解 TopK Elements 问题,通过维护一个大小为 K 的堆,堆中的元素就是 TopK Elements。当然它也可以用于求解 Kth Element 问题,堆顶元素就是 Kth Element。快速选择也可以求解 TopK Elements 问题,因为找到 Kth Element 之后,再遍历一次数组,所有小于等于 Kth Element 的元素都是 TopK Elements。可以看到,快速选择和堆排序都可以求解 Kth Element 和 TopK Elements 问题。 排序 :时间复杂度 O(NlogN),空间复杂度 O(1) public int findKthLargest(int[] nums, int k) { Arrays.sort(nums); return nums[nums.length - k]; } 堆排序 :时间复杂度 O(NlogK),空间复杂度 O(K)。 每次插入一个元素,当元素超过k个时,弹出顶部的最小值,当元素push完以后,剩下的元素就是前k大的元素,堆顶元素就是第K大的元素。 public int findKthLargest(int[] nums, int k) { PriorityQueue<Integer> pq = new PriorityQueue<>(); // 小顶堆 for (int val : nums) { pq.add(val); if (pq.size() > k) // 维护堆的大小为 K pq.poll(); } return pq.peek(); } 快速选择(也可以认为是快速排序的partition加上二分的算法) 利用partition函数求出一个数的最终位置,再通过二分来逼近第k个位置,算法结论表明该算法的时间复杂度是O(N) class Solution { public int findKthLargest(int[] nums, int k) { k = nums.length - k; int l = 0, r = nums.length - 1; while (l < r) { int pos = partition(nums, l , r); if (pos == k) return nums[pos]; else if (pos < k) { l = pos + 1; }else { r = pos - 1; } } return nums[k]; } public int partition(int[] nums, int left, int right) { int l = left, r = right; int temp = nums[l]; while (l < r) { while (l < r && nums[r] >= temp) { r --; } while (l < r && nums[l] <= temp) { l ++; } if (l < r) { int tmp = nums[l]; nums[l] = nums[r]; nums[r] = tmp; } } nums[left] = nums[l]; nums[l] = temp; return l; } } 桶排序 前K个高频元素 给定一个非空的整数数组,返回其中出现频率前 k 高的元素。 例如, 给定数组 [1,1,1,2,2,3] , 和 k = 2,返回 [1,2]。 注意: 你可以假设给定的 k 总是合理的,1 ≤ k ≤ 数组中不相同的元素的个数。 你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。 解析: 设置若干个桶,每个桶存储出现频率相同的数,并且桶的下标代表桶中数出现的频率,即第 i 个桶中存储的数出现的频率为 i。把数都放到桶之后,从后向前遍历桶,最先得到的 k 个数就是出现频率最多的的 k 个数。 import java.util.*; class Solution { public List<Integer> topKFrequent(int[] nums, int k) { Map<Integer, Integer> map = new HashMap<>(); for (int i : nums) { if (map.containsKey(i)) { map.put(i, map.get(i) + 1); }else { map.put(i, 1); } } ArrayList<Integer>[] timesMap = new ArrayList[nums.length + 1]; for (int key : map.keySet()) { int times = map.get(key); if (timesMap[times] == null) { timesMap[times] = new ArrayList<>(); timesMap[times].add(key); } else { timesMap[times].add(key); } } List<Integer> top = new ArrayList<Integer>(); for (int i = timesMap.length - 1;i > 0 && top.size() < k;i --) { if (timesMap[i] != null) { top.addAll(timesMap[i]); } } return top; } } 注意: 1本题的难点在于先用hashmap存储数据得到每个数的频率,再用数组存储每个频率对应哪些数。 2最后再通过频率数组的最后一位开始往前找,找到k个数为之,就是出现频率最高的k个数了。 根据字符出现频率排序 给定一个字符串,请将字符串里的字符按照出现的频率降序排列。 输入: “tree” 输出: “eert” 解释: ‘e’出现两次,’r’和’t’都只出现一次。 因此’e’必须出现在’r’和’t’之前。此外,”eetr”也是一个有效的答案。 我下面这个写法只考虑了小写字母的情况,大写字母与其他字符没有考虑,是错误的。正确的做法还是应该用一个128长度的char数组 。因为char是1一个字节长度,也就是8位,2的8次方是256,考虑正数的话就是128。 上题使用map是因为32位整数太大,数组存不下,而本题char数组只需要长度为128即可,不用使用map。 错误解: public static String frequencySort(String s) { int []arr = new int[26]; char []crr = s.toCharArray(); for (char c : crr) { arr[c - 'a']++; } List<Character>[]times = new ArrayList[s.length() + 1]; for (int i = 0;i < arr.length;i ++) { if (times[arr[i]] == null) { times[arr[i]] = new ArrayList<>(); times[arr[i]].add((char) ('a' + i)); }else { times[arr[i]].add((char) ('a' + i)); } } StringBuilder sb = new StringBuilder(); for (int i = times.length - 1;i > 0 ;i --) { if (times[i] != null) { for (char c : times[i]) { int time = 0; while (time < i) { sb.append(c); time ++; } } } } return sb.toString(); } 正解: class Solution { public static String frequencySort(String s) { int []arr = new int[128]; char []crr = s.toCharArray(); for (char c : crr) { arr[c]++; } List<Character>[]times = new ArrayList[s.length() + 1]; for (int i = 0;i < arr.length;i ++) { if (times[arr[i]] == null) { times[arr[i]] = new ArrayList<>(); times[arr[i]].add((char) (i)); }else { times[arr[i]].add((char) (i)); } } StringBuilder sb = new StringBuilder(); for (int i = times.length - 1;i > 0 ;i --) { if (times[i] != null) { for (char c : times[i]) { int time = 0; while (time < i) { sb.append(c); time ++; } } } } return sb.toString(); } } 分类颜色 给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。 此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。 注意: 不能使用代码库中的排序函数来解决这道题。 进阶: 一个直观的解决方案是使用计数排序的两趟扫描算法。 首先,迭代计算出0、1 和 2 元素的个数,然后按照0、1、2的排序,重写当前数组。 你能想出一个仅使用常数空间的一趟扫描算法吗? 解析:本题的思路一个就是题目所说的计数排序,还有一个便是使用交换算法,设置三个下标,zero, one, two,分别表示0的结尾,1的结尾,2的结尾,并且在遍历过程中把0换到one前面,把2换到one后面,中间的就是1了。 class Solution { public void sortColors(int[] nums) { if (nums.length <= 1)return; int zero = -1, one = 0,two = nums.length; while (one < two) { if (nums[one] == 0) { swap(nums, ++zero, one++); }else if (nums[one] == 2) { swap(nums, --two, one); }else { one ++; } } } public void swap(int []nums, int i, int j) { int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; } } 二分查找 正常实现 public int binarySearch(int[] nums, int key) { int l = 0, h = nums.length - 1; while (l <= h) { int m = l + (h - l) / 2; if (nums[m] == key) { return m; } else if (nums[m] > key) { h = m - 1; } else { l = m + 1; } } return -1; } 时间复杂度 二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度都为 O(logN)。 m 计算 有两种计算中值 m 的方式: m = (l + h) / 2 m = l + (h - l) / 2 l + h 可能出现加法溢出,最好使用第二种方式。 返回值 循环退出时如果仍然没有查找到 key,那么表示查找失败。可以有两种返回值: -1:以一个错误码表示没有查找到 key l:将 key 插入到 nums 中的正确位置 变种 题目:在一个有重复元素的数组中查找 key 的最左位置 如果是直接查找那么复杂度为O(n)所以可以采用二分优化 二分查找可以有很多变种,变种实现要注意边界值的判断。 例如在一个有重复元素的数组中查找 key 的最左位置的实现如下: public int binarySearch(int[] nums, int key) { int l = 0, h = nums.length - 1; while (l < h) { int m = l + (h - l) / 2; if (nums[m] >= key) { h = m; } else { l = m + 1; } } return l; } 该实现和正常实现有以下不同: 循环条件为 l < h h 的赋值表达式为 h = m 最后返回 l 而不是 -1 在 nums[m] >= key 的情况下,可以推导出最左 key 位于 [l, m] 区间中,这是一个闭区间。h 的赋值表达式为 h = m,因为 m 位置也可能是解。 在 h 的赋值表达式为 h = mid 的情况下,如果循环条件为 l <= h,那么会出现循环无法退出的情况,因此循环条件只能是 l < h。以下演示了循环条件为 l <= h 时循环无法退出的情况: nums = {0, 1, 2}, key = 1 l m h 0 1 2 nums[m] >= key 0 0 1 nums[m] < key 1 1 1 nums[m] >= key 1 1 1 nums[m] >= key … 当循环体退出时,不表示没有查找到 key,因此最后返回的结果不应该为 -1。为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等 x 的平方根 实现 int sqrt(int x) 函数。 计算并返回 x 的平方根,其中 x 是非负整数。 由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。 示例 1: 输入: 4 输出: 2 示例 2: 输入: 8 输出: 2 说明: 8 的平方根是 2.82842…, 由于返回类型是整数,小数部分将被舍去。 一个数 x 的开方 sqrt 一定在 0 ~ x 之间,并且满足 sqrt == x / sqrt。可以利用二分查找在 0 ~ x 之间查找 sqrt。 对于 x = 8,它的开方是 2.82842…,最后应该返回 2 而不是 3。在循环条件为 l <= h 并且循环退出时,h 总是比 l 小 1,也就是说 h = 2,l = 3,因此最后的返回值应该为 h 而不是 l。 public int mySqrt(int x) { if (x <= 1) { return x; } int l = 1, h = x; while (l <= h) { int mid = l + (h - l) / 2; int sqrt = x / mid; if (sqrt == mid) { return mid; } else if (mid > sqrt) { h = mid - 1; } else { l = mid + 1; } } return h; } 注意:由于要取的值是比原值小的整数,所以等sqrt小于mid时,并且此时l > h时说明h此时已经是最接近sqrt且比它小的值了。当然如果前面有相等的情况时已经返回了。 744. 寻找比目标字母大的最小字母 给定一个只包含小写字母的有序数组letters 和一个目标字母 target,寻找有序数组里面比目标字母大的最小字母。 数组里字母的顺序是循环的。举个例子,如果目标字母target = ‘z’ 并且有序数组为 letters = [‘a’, ‘b’],则答案返回 ‘a’。 示例: 输入: letters = [“c”, “f”, “j”] target = “a” 输出: “c” 输入: letters = [“c”, “f”, “j”] target = “c” 输出: “f” 输入: letters = [“c”, “f”, “j”] target = “d” 输出: “f” 输入: letters = [“c”, “f”, “j”] target = “g” 输出: “j” 输入: letters = [“c”, “f”, “j”] target = “j” 输出: “c” 输入: letters = [“c”, “f”, “j”] target = “k” 输出: “c” 注: letters长度范围在[2, 10000]区间内。 letters 仅由小写字母组成,最少包含两个不同的字母。 目标字母target 是一个小写字母。 解析:使用二分查找逼近,找到字母后右边那个就是最小的,找不到的话返回结束位置的右边第一个字母。 注意: 1 与上一题相反,本题的要找的是比指定值大一点的数,所以此时l > r满足时,l就是比指定值大一点的数了。 2 注意可能有连续重复的数字,所以一直往右找到一个数大于指定值 class Solution { public char nextGreatestLetter(char[] letters, char target) { if (letters == null || letters.length == 0) return 'a'; int l = 0,r = letters.length - 1; while (l <= r) { int m = l + (r - l)/2; if (letters[m] <= target ) { l = m + 1; }else { r = m - 1; } } if (l <= letters.length - 1) { return letters[l]; }else { return letters[0]; } } } 有序数组中的单一元素 给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。 示例 1: 输入: [1,1,2,3,3,4,4,8,8] 输出: 2 示例 2: 输入: [3,3,7,7,10,11,11] 输出: 10 注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。 解析:本题其实可以用位运算做,但是限制了时间复杂度,所以考虑使用二分,这题我做不出来,可以参考下面答案 令 index 为 Single Element 在数组中的位置。如果 m 为偶数,并且 m + 1 < index,那么 nums[m] == nums[m + 1];m + 1 >= index,那么 nums[m] != nums[m + 1]。 从上面的规律可以知道,如果 nums[m] == nums[m + 1],那么 index 所在的数组位置为 [m + 2, h],此时令 l = m + 2;如果 nums[m] != nums[m + 1],那么 index 所在的数组位置为 [l, m],此时令 h = m。 因为 h 的赋值表达式为 h = m,那么循环条件也就只能使用 l < h 这种形式。 public int singleNonDuplicate(int[] nums) { int l = 0, h = nums.length - 1; while (l < h) { int m = l + (h - l) / 2; if (m % 2 == 1) { m--; // 保证 l/h/m 都在偶数位,使得查找区间大小一直都是奇数 } if (nums[m] == nums[m + 1]) { l = m + 2; } else { h = m; } } return nums[l]; } 153. 寻找旋转排序数组中的最小值 假设按照升序排序的数组在预先未知的某个点上进行了旋转。 ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。 请找出其中最小的元素。 你可以假设数组中不存在重复元素。 示例 1: 输入: [3,4,5,1,2] 输出: 1 示例 2: 输入: [4,5,6,7,0,1,2] 输出: 0 解析:比较经典的题目,正常情况下是顺序的,仅当arr[i] > arr[i + 1]可以得知arr[i + 1]是最小值。 顺序扫描需要O(n),使用二分查找可以优化到Log2n 旋转数组的两个递增数组由最小值来划分。 所以对于l, m, r来说,如果arr[m] < arr[h],说明到m到h是有序部分,最小值应该在l到m之间。所以令r = m; 如果arr[h] < arr[m],说明最小值在m到h之间。所以令l = m + 1。 当l > r时,说明nums[m] > nums[h]已经到达终点,此时nums[m + 1 ]就是最小值 public int findMin(int[] nums) { int l = 0, h = nums.length - 1; while (l < h) { int m = l + (h - l) / 2; if (nums[m] <= nums[h]) { h = m; } else { l = m + 1; } } return nums[l]; } 在排序数组中查找元素的第一个和最后一个位置 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。 你的算法时间复杂度必须是 O(log n) 级别。 如果数组中不存在目标值,返回 [-1, -1]。 示例 1: 输入: nums = [5,7,7,8,8,10], target = 8 输出: [3,4] 示例 2: 输入: nums = [5,7,7,8,8,10], target = 6 输出: [-1,-1] 解析:参考别人的答案: 1 首先通过二分查找找到该数出现的最左边位置(与例题一样) 2 然后通过二分查找找到比该数大1的数出现的位置,如果不存在,则刚好在所求数右边一位,再减1即可。 3 边界条件判断 public int[] searchRange(int[] nums, int target) { int first = binarySearch(nums, target); int last = binarySearch(nums, target + 1) - 1; if (first == nums.length || nums[first] != target) { return new int[]{-1, -1}; } else { return new int[]{first, Math.max(first, last)}; } } private int binarySearch(int[] nums, int target) { int l = 0, h = nums.length; // 注意 h 的初始值 while (l < h) { int m = l + (h - l) / 2; if (nums[m] >= target) { h = m; } else { l = m + 1; } } return l; } DFS和BFS,回溯 搜索 深度优先搜索和广度优先搜索广泛运用于树和图中,但是它们的应用远远不止如此。 BFS 广度优先搜索的搜索过程有点像一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。 第一层: 0 -> {6,2,1,5}; 第二层: 6 -> {4} 2 -> {} 1 -> {} 5 -> {3} 第三层: 4 -> {} 3 -> {} 可以看到,每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di<=dj。利用这个结论,可以求解最短路径等 最优解 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径。 在程序实现 BFS 时需要考虑以下问题: 队列:用来存储每一轮遍历得到的节点; 标记:对于遍历过的节点,应该将它标记,防止重复遍历。 计算在网格中从原点到特定点的最短路径长度 [[1,1,0,1], [1,0,1,0], [1,1,1,1], [1,0,1,1]] 1 表示可以经过某个位置,求解从 (0, 0) 位置到 (tr, tc) 位置的最短路径长度。 2 由于每个点需要保存x坐标,y坐标以及长度,所以必须要用一个类将三个属性封装起来。 3 由于bfs每次只将距离加一,所以当位置抵达终点时,此时的距离就是最短路径了。 private static class Position { int r, c, length; public Position(int r, int c, int length) { this.r = r; this.c = c; this.length = length; } } public static int minPathLength(int[][] grids, int tr, int tc) { int[][] next = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; int m = grids.length, n = grids[0].length; Queue<Position> queue = new LinkedList<>(); queue.add(new Position(0, 0, 1)); while (!queue.isEmpty()) { Position pos = queue.poll(); for (int i = 0; i < 4; i++) { Position nextPos = new Position(pos.r + next[i][0], pos.c + next[i][1], pos.length + 1); if (nextPos.r < 0 || nextPos.r >= m || nextPos.c < 0 || nextPos.c >= n) continue; if (grids[nextPos.r][nextPos.c] != 1) continue; grids[nextPos.r][nextPos.c] = 0; if (nextPos.r == tr && nextPos.c == tc) return nextPos.length; queue.add(nextPos); } } return -1; } 完全平方数 组成整数的最小平方数数量 给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。 示例 1: 输入: n = 12 输出: 3 解释: 12 = 4 + 4 + 4. 示例 2: 输入: n = 13 输出: 2 解释: 13 = 4 + 9. 1 可以将每个整数看成图中的一个节点,如果两个整数之差为一个平方数,那么这两个整数所在的节点就有一条边。 2 要求解最小的平方数数量,就是求解从节点 n 到节点 0 的最短路径。 3 首先生成平方数序列放入数组,然后通过队列,每次减去一个平方数,把剩下的数加入队列,也就是通过bfs的方式,当此时的数刚好等于平方数,则满足题意,由于每次循环level加一,所以最后输出的level就是需要的平方数个数。 本题也可以用动态规划求解,在之后动态规划部分中会再次出现。 public int numSquares(int n) { List<Integer> squares = generateSquares(n); Queue<Integer> queue = new LinkedList<>(); boolean[] marked = new boolean[n + 1]; queue.add(n); marked[n] = true; int level = 0; while (!queue.isEmpty()) { int size = queue.size(); level++; while (size-- > 0) { int cur = queue.poll(); for (int s : squares) { int next = cur - s; if (next < 0) { break; } if (next == 0) { return level; } if (marked[next]) { continue; } marked[next] = true; queue.add(cur - s); } } } return n; } /** * 生成小于 n 的平方数序列 * @return 1,4,9,... */ private List<Integer> generateSquares(int n) { List<Integer> squares = new ArrayList<>(); int square = 1; int diff = 3; while (square <= n) { squares.add(square); square += diff; diff += 2; } return squares; } 127. 单词接龙 给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则: 每次转换只能改变一个字母。 转换过程中的中间单词必须是字典中的单词。 说明: 如果不存在这样的转换序列,返回 0。 所有单词具有相同的长度。 所有单词只由小写字母组成。 字典中不存在重复的单词。 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。 示例 1: 输入: beginWord = “hit”, endWord = “cog”, wordList = [“hot”,”dot”,”dog”,”lot”,”log”,”cog”] 输出: 5 解释: 一个最短转换序列是 “hit” -> “hot” -> “dot” -> “dog” -> “cog”, 返回它的长度 5。 示例 2: 输入: beginWord = “hit” endWord = “cog” wordList = [“hot”,”dot”,”dog”,”lot”,”log”] 输出: 0 解释: endWord “cog” 不在字典中,所以无法进行转换。 找出一条从 beginWord 到 endWord 的最短路径,每次移动规定为改变一个字符,并且改变之后的字符串必须在 wordList 中。 单词台阶问题,亚马逊面试时考了。 这个参考别人的答案,我会加上解析。 public int ladderLength(String beginWord, String endWord, List<String> wordList) { //注意此处把首个单词放到了list的最后面,所以start才会是N-1。别搞错了。 wordList.add(beginWord); int N = wordList.size(); int start = N - 1; int end = 0; while (end < N && !wordList.get(end).equals(endWord)) { end++; } if (end == N) { return 0; } List<Integer>[] graphic = buildGraphic(wordList); return getShortestPath(graphic, start, end); } 本方法用于把每个单词开头的完整序列保存起来,以便让bfs过程中遍历到所有情况。 private List<Integer>[] buildGraphic(List<String> wordList) { int N = wordList.size(); List<Integer>[] graphic = new List[N]; for (int i = 0; i < N; i++) { graphic[i] = new ArrayList<>(); for (int j = 0; j < N; j++) { if (isConnect(wordList.get(i), wordList.get(j))) { graphic[i].add(j); } } } return graphic; } 本方法用于上面这个方法连接单词序列时,需要判断两个单词是否只需要一次改变即可,如果不满足要求,则跳过这个单词。 private boolean isConnect(String s1, String s2) { int diffCnt = 0; for (int i = 0; i < s1.length() && diffCnt <= 1; i++) { if (s1.charAt(i) != s2.charAt(i)) { diffCnt++; } } return diffCnt == 1; } 这一步就是通过BFS进行单词序列连接了。 让初始所在位置入队,然后去遍历它能转变成的单词,接着进行bfs的遍历。 最终当next = end时,说明已经能到达最终位置了。所以此时的路径时最短的。每次出队都是一个路径,所以返回path即为最短路径长度。 private int getShortestPath(List<Integer>[] graphic, int start, int end) { Queue<Integer> queue = new LinkedList<>(); boolean[] marked = new boolean[graphic.length]; queue.add(start); marked[start] = true; int path = 1; while (!queue.isEmpty()) { int size = queue.size(); path++; while (size-- > 0) { int cur = queue.poll(); for (int next : graphic[cur]) { if (next == end) { return path; } if (marked[next]) { continue; } marked[next] = true; queue.add(next); } } } return 0; } DFS 广度优先搜索一层一层遍历,每一层得到的所有新节点,要用队列存储起来以备下一层遍历的时候再遍历。 而深度优先搜索在得到一个新节点时立马对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。 从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 可达性 问题。 在程序实现 DFS 时需要考虑以下问题: 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。 岛屿的最大面积 给定一个包含了一些 0 和 1的非空二维数组 grid , 一个 岛屿 是由四个方向 (水平或垂直) 的 1 (代表土地) 构成的组合。你可以假设二维矩阵的四个边缘都被水包围着。 找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为0。) 示例 1: [[0,0,1,0,0,0,0,1,0,0,0,0,0], [0,0,0,0,0,0,0,1,1,1,0,0,0], [0,1,1,0,1,0,0,0,0,0,0,0,0], [0,1,0,0,1,1,0,0,1,0,1,0,0], [0,1,0,0,1,1,0,0,1,1,1,0,0], [0,0,0,0,0,0,0,0,0,0,1,0,0], [0,0,0,0,0,0,0,1,1,1,0,0,0], [0,0,0,0,0,0,0,1,1,0,0,0,0]] 对于上面这个给定矩阵应返回 6。注意答案不应该是11,因为岛屿只能包含水平或垂直的四个方向的‘1’。 示例 2: [[0,0,0,0,0,0,0,0]] 对于上面这个给定的矩阵, 返回 0。 注意: 给定的矩阵grid 的长度和宽度都不超过 50。 //只需要从每个1出发,然后遍历相连的所有1,得到总和,更新最大值即可。 public static int maxAreaOfIsland(int[][] grid) { int [][]visit = new int[grid.length][grid[0].length]; int max = 0; for (int i = 0;i < grid.length;i ++) { for (int j = 0;j < grid[0].length;j ++) { if (grid[i][j] == 1) { max = Math.max(max, dfs(grid, i, j, visit, 0)); } } } return max; } //通过递归进行了各个方向的可达性遍历,于是可以遍历到所有的1,然后更新最大值。 public static int dfs(int [][]grid, int x, int y, int [][]visit, int count) { if (x < 0 || x > grid.length - 1 || y < 0 || y > grid[0].length - 1) { return count; } if (visit[x][y] == 1 || grid[x][y] == 0) { return count; } visit[x][y] = 1; count ++; count += dfs(grid, x + 1, y, visit, 0); count += dfs(grid, x - 1, y, visit, 0); count += dfs(grid, x, y + 1, visit, 0); count += dfs(grid, x, y - 1, visit, 0); return count; } 岛屿的个数 给定一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。 示例 1: 输入: 11110 11010 11000 00000 输出: 1 示例 2: 输入: 11000 11000 00100 00011 输出: 3 public class 图的连通分量个数 { static int count = 0; public int findCircleNum(int[][] M) { count = 0; int []visit = new int[M.length]; Arrays.fill(visit, 0); for (int i = 0;i < M.length;i ++) { if (visit[i] == 0) { dfs(M, i, visit); count ++; } } return count; } //每次访问把能到达的点标记为1,并且访问结束时计数加一。最终得到岛屿个数。 public void dfs (int [][]M, int j, int []visit) { for (int i = 0;i < M.length;i ++) { if (M[j][i] == 1 && visit[i] == 0) { visit[i] = 1; dfs(M, i, visit); } } } } 朋友圈 班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。 给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。 示例 1: 输入: [[1,1,0], [1,1,0], [0,0,1]] 输出: 2 说明:已知学生0和学生1互为朋友,他们在一个朋友圈。 第2个学生自己在一个朋友圈。所以返回2。 示例 2: 输入: [[1,1,0], [1,1,1], [0,1,1]] 输出: 1 说明:已知学生0和学生1互为朋友,学生1和学生2互为朋友,所以学生0和学生2也是朋友,所以他们三个在一个朋友圈,返回1。 注意: N 在[1,200]的范围内。 对于所有学生,有M[i][i] = 1。 如果有M[i][j] = 1,则有M[j][i] = 1。 这题的答案是这样的: private int n; public int findCircleNum(int[][] M) { n = M.length; int circleNum = 0; boolean[] hasVisited = new boolean[n]; for (int i = 0; i < n; i++) { if (!hasVisited[i]) { dfs(M, i, hasVisited); circleNum++; } } return circleNum; } private void dfs(int[][] M, int i, boolean[] hasVisited) { hasVisited[i] = true; for (int k = 0; k < n; k++) { if (M[i][k] == 1 && !hasVisited[k]) { dfs(M, k, hasVisited); } } } 但是我的做法跟他一样,却会递归栈溢出,我只是把boolean判断换成了int判断,有点奇怪,还望指教。 // private static int n; // public static int findCircleNum(int[][] M) { // n = M.length; // int cnt = 0 ; // int []visit = new int[n]; // for (int i = 0;i < M.length;i ++) { // if(visit[i] == 0) { // dfs(M, visit, i); // cnt ++; // } // } // return cnt; // } // public static void dfs(int[][]M, int[] visit, int i) { // visit[i] = 1; // for (int j = 0;j < M.length;j ++) { // if(visit[j] == 0 && M[i][j] == 1) { // dfs(M, visit, i); // } // } // } 被围绕的区域 给定一个二维的矩阵,包含 ‘X’ 和 ‘O’(字母 O)。 找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。 示例: X X X X X O O X X X O X X O X X 运行你的函数后,矩阵变为: X X X X X X X X X X X X X O X X 解释: 被围绕的区间不会存在于边界上,换句话说,任何边界上的 ‘O’ 都不会被填充为 ‘X’。 任何不在边界上,或不与边界上的 ‘O’ 相连的 ‘O’ 最终都会被填充为 ‘X’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。 参考大佬答案:很有意思的解法 1 我们是要把X包围的O变成X但是有一个很麻烦的问题就是,如何判断O被X完全包住,这是非常难解决的。 2 于是换一种思路,把不被X包住的那些O找出来,剩下的不就是X了吗。 3 不被X包住的O,首先它们的起点一定是在边缘处,所以我们从边缘处找出一个O,然后从O出发,找到所有相连的O,把它们变成T(为了不跟里面的O混淆) 4 最后遍历一次棋盘,把T变成O,把O变成X,就搞定了。妙啊,妙啊。 private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; private int m, n; public void solve(char[][] board) { if (board == null || board.length == 0) { return; } m = board.length; n = board[0].length; for (int i = 0; i < m; i++) { dfs(board, i, 0); dfs(board, i, n - 1); } for (int i = 0; i < n; i++) { dfs(board, 0, i); dfs(board, m - 1, i); } for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (board[i][j] == 'T') { board[i][j] = 'O'; } else if (board[i][j] == 'O') { board[i][j] = 'X'; } } } } private void dfs(char[][] board, int r, int c) { if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != 'O') { return; } board[r][c] = 'T'; for (int[] d : direction) { dfs(board, r + d[0], c + d[1]); } } 417.能到达的太平洋和大西洋的区域 Pacific Atlantic Water Flow (Medium) Given the following 5x5 matrix: Pacific ~ ~ ~ ~ ~ ~ 1 2 2 3 (5) * ~ 3 2 3 (4) (4) * ~ 2 4 (5) 3 1 * ~ (6) (7) 1 4 5 * ~ (5) 1 1 2 4 * * * * * * Atlantic Return: [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (positions with parentheses in above matrix). 左边和上边是太平洋,右边和下边是大西洋,内部的数字代表海拔,海拔高的地方的水能够流到低的地方,求解水能够流到太平洋和大西洋的所有位置。 1 如果说上一题已经很有趣了,这一题可以说是更奇葩了。 力扣中国甚至没有翻译这道题。根据题意,我们要求的是水能流到太平洋和大西洋的所有点。 2 首先,在大西洋和 太平洋两边的水一定可以分别流入这两个海洋。 我们用一个数组canreach[i][j]来表达能够流入到海洋。所以我么需要两个数组。 3 然后从海洋边上的水开始进行dfs,遇到海拔比自己高的水就把它也设置成canreach即可,于是我们就可以得到两个数组。最后遍历两个数组,都满足的点就是结果了。 private int m, n; private int[][] matrix; private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; public List<int[]> pacificAtlantic(int[][] matrix) { List<int[]> ret = new ArrayList<>(); if (matrix == null || matrix.length == 0) { return ret; } m = matrix.length; n = matrix[0].length; this.matrix = matrix; boolean[][] canReachP = new boolean[m][n]; boolean[][] canReachA = new boolean[m][n]; for (int i = 0; i < m; i++) { dfs(i, 0, canReachP); dfs(i, n - 1, canReachA); } for (int i = 0; i < n; i++) { dfs(0, i, canReachP); dfs(m - 1, i, canReachA); } for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (canReachP[i][j] && canReachA[i][j]) { ret.add(new int[]{i, j}); } } } return ret; } private void dfs(int r, int c, boolean[][] canReach) { if (canReach[r][c]) { return; } canReach[r][c] = true; for (int[] d : direction) { int nextR = d[0] + r; int nextC = d[1] + c; if (nextR < 0 || nextR >= m || nextC < 0 || nextC >= n || matrix[r][c] > matrix[nextR][nextC]) { continue; } dfs(nextR, nextC, canReach); } } Backtracking Backtracking(回溯)属于 DFS。 普通 DFS 主要用在 可达性问题 ,这种问题只需要执行到特点的位置然后返回即可。 而 Backtracking 主要用于求解 排列组合 问题,例如有 { ‘a’,’b’,’c’ } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。 因为 Backtracking 不是立即就返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题: 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素; 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。 电话号码的字母组合 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 示例: 输入:”23” 输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”]. 解析:回溯法一般用于需要保存结果集的dfs,比如迷宫路径,走到头如果没有达到终点则回头。而一般的dfs走到头就开始尝试下一种情况,因为它没有保存结果集。 1 本题需要把数字键盘和字母做一个映射,使用数组是一个不错的办法。而使用hashmap可能会显得有点臃肿。 2 接着我们可以使用String或者Stringbuilder保存结果,由于string不会被改变,所以我们不需要维持其状态,直接递归即可。 而使用stringbuilder则需要在dfs前后维护相应变化。 class Solution { public List<String> letterCombinations(String digits) { if (digits.equals("")) { return new ArrayList(); } List<String> list = new ArrayList<>(); String []arr = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; dfs(list, arr, new StringBuilder(), 0, digits); return list; } public void dfs(List<String> list, String []arr, StringBuilder sb, int cur, String digits) { if (cur == digits.length()) { list.add(sb.toString()); return; } int num = Integer.parseInt(digits.substring(cur, cur + 1)); for (int i = 0;i < arr[num].length();i ++) { sb.append(arr[num].charAt(i)); dfs(list, arr, sb, cur + 1, digits); sb.deleteCharAt(sb.length() - 1); } } } 复原IP地址 给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。 示例: 输入: “25525511135” 输出: [“255.255.11.135”, “255.255.111.35”] 解析:本题的解题思路不难,从左到右拼凑ip地址即可 但是需要判断每个段的ip是否有效,包括开头不能是0,长度<=3,大小<=255等。 下面是一个暴力解法,4重循环可以遍历所有情况。 public List<String> restoreIpAddresses(String s) { int i,j,k,m; ArrayList <String> list = new ArrayList<>(); int len = s.length(); for (i = 1;i< 4 && i < len - 2;i ++) { for (j = i + 1;j < i + 4 && j < len - 1;j ++) { for (m = j + 1;m < j + 4 && m < len;m ++) { //substring后面的下标是不算在内的。 String s1 = s.substring(0,i); String s2 = s.substring(i,j); String s3 = s.substring(j,m); String s4 = s.substring(m,len); if(isValid(s1) && isValid(s2) && isValid(s3) && isValid(s4)) { list.add(s1 + '.' + s2 + '.' + s3 + '.' + s4); } } } } return list; } public boolean isValid(String s) { if (s.length() == 0 || s.length() > 3 || s.charAt(0) == '0' && s.length() > 1 || Integer.parseInt(s) > 255) { return false; }else return true; } 解析: 使用回溯法来做该题。首先从头开始,k代表ip的段数,s代表总长度,每次使用一个数字则s -= 1,直到s = 0并且k = 4时,符合题意,加入结果集。 当然在for循环中,i 从0到2进行遍历。然后更新当前结果。 public List<String> restoreIpAddresses(String s) { List<String> addresses = new ArrayList<>(); StringBuilder tempAddress = new StringBuilder(); doRestore(0, tempAddress, addresses, s); return addresses; } private void doRestore(int k, StringBuilder tempAddress, List<String> addresses, String s) { if (k == 4 || s.length() == 0) { if (k == 4 && s.length() == 0) { addresses.add(tempAddress.toString()); } return; } for (int i = 0; i < s.length() && i <= 2; i++) { if (i != 0 && s.charAt(0) == '0') { break; } String part = s.substring(0, i + 1); if (Integer.valueOf(part) <= 255) { if (tempAddress.length() != 0) { part = "." + part; } tempAddress.append(part); doRestore(k + 1, tempAddress, addresses, s.substring(i + 1)); tempAddress.delete(tempAddress.length() - part.length(), tempAddress.length()); } } } 单词搜索 给定一个二维网格和一个单词,找出该单词是否存在于网格中。 单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。 示例: board = [ [‘A’,’B’,’C’,’E’], [‘S’,’F’,’C’,’S’], [‘A’,’D’,’E’,’E’] ] 给定 word = “ABCCED”, 返回 true. 给定 word = “SEE”, 返回 true. 给定 word = “ABCB”, 返回 false. 本题使用dfs中的回溯法,主要思路也是递归。 但比较麻烦的一点是当遍历到满足条件的情况时,不应该继续其他递归分支了。有一个办法是使用for循环进行方向遍历,只要有一个分支满足就返回true。 private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; private int m; private int n; public boolean exist(char[][] board, String word) { if (word == null || word.length() == 0) { return true; } if (board == null || board.length == 0 || board[0].length == 0) { return false; } m = board.length; n = board[0].length; boolean[][] hasVisited = new boolean[m][n]; for (int r = 0; r < m; r++) { for (int c = 0; c < n; c++) { if (backtracking(0, r, c, hasVisited, board, word)) { return true; } } } return false; } private boolean backtracking(int curLen, int r, int c, boolean[][] visited, final char[][] board, final String word) { if (curLen == word.length()) { return true; } if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != word.charAt(curLen) || visited[r][c]) { return false; } visited[r][c] = true; for (int[] d : direction) { //此处完成了剪枝,如果分支i满足则不会走后续分支。 if (backtracking(curLen + 1, r + d[0], c + d[1], visited, board, word)) { return true; } } visited[r][c] = false; return false; } 解析:方法二: 首先使用dfs进行递归遍历,当满足条件时,直接抛出异常退出循环,脑洞比较大,但是确实可行,可以用于递归时的剪枝 。 但是实际上时间复杂度要比第一种方法大得多,这里只是提供一个终止递归的思路。 class Solution { static class StopMsgException extends RuntimeException { } static boolean flag; public boolean exist(char[][] board, String word) { if (word.equals("")) { return true; } int [][]visit = new int[board.length][board[0].length]; flag = false; try { for (int i = 0;i < board.length;i ++) { for (int j = 0;j < board[0].length;j ++) { if (word.charAt(0) == board[i][j]) { dfs(board, word, visit, i, j); } } } } catch (StopMsgException e) { System.out.println(e); } return flag; } public void dfs(char[][] board, String word, int [][]visit, int i, int j) { if (word.equals("")) { flag = true; throw new StopMsgException(); } if (i > board.length - 1 || i < 0 || j > board[0].length - 1 || j < 0) { return; } if (visit[i][j] == 1) { return; } if (word.charAt(0) == board[i][j]) { visit[i][j] = 1; //没有进行剪枝,效率比较低,于是在递归判断条件中进行剪枝,避免后续不必要的递归。 dfs(board, word.length() == 1 ? "" : word.substring(1, word.length()), visit, i + 1, j); dfs(board, word.length() == 1 ? "" : word.substring(1, word.length()), visit, i - 1, j); dfs(board, word.length() == 1 ? "" : word.substring(1, word.length()), visit, i, j - 1); dfs(board, word.length() == 1 ? "" : word.substring(1, word.length()), visit, i, j + 1); visit[i][j] = 0; } } } 全排列 给定一个没有重复数字的序列,返回其所有可能的全排列。 示例: 输入: [1,2,3] 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ] 解析:经典而不失优雅。dfs中使用for循环遍历,visit和list记录状态进行回溯,满足条件时加入集合并返回。 class Solution { static List<List<Integer>> Alllist = new ArrayList<>(); public List<List<Integer>> permute(int[] nums) { Alllist.clear(); List<Integer> list = new ArrayList<>(); int []visit = new int[nums.length]; dfs(list, nums, visit); return Alllist; } public void dfs(List<Integer> list, int []nums, int []visit) { if (list.size() == nums.length) { Alllist.add(new ArrayList(list)); return; } for (int i = 0;i < nums.length;i ++) { if (visit[i] == 0) { visit[i] = 1; list.add(nums[i]); dfs(list, nums, visit); visit[i] = 0; list.remove(list.size() - 1); } } } } 全排列 II 给定一个可包含重复数字的序列,返回所有不重复的全排列。 示例: 输入: [1,1,2] 输出: [ [1,1,2], [1,2,1], [2,1,1] ] 解析:本题在上一题的基础上加上了一个条件,数组中有重复数字,但是结果集返回的是不同的排列。 这就要求我们对相同数字做过滤了。 我们要明确的是,不重复排列,要求的是相同数字不能在同一次递归中出现在同一个位置。 比如 1 2 1 和1 2 1,这里开头的两个1可能分别对应数组下标的0和1,但只能取一个。 所以我们加了一个条件,当该数没有被访问过时,我们直接过滤掉所有重复的数,只把当前数作为本次递归的首位数。 class Solution { static List<List<Integer>> Alllist = new ArrayList<>(); public List<List<Integer>> permuteUnique(int[] nums) { Alllist.clear(); List<Integer> list = new ArrayList<>(); int []visit = new int[nums.length]; Arrays.sort(nums); dfs(list, nums, visit); return Alllist; } public void dfs(List<Integer> list, int []nums, int []visit) { if (list.size() == nums.length) { Alllist.add(new ArrayList(list)); return; } for (int i = 0;i < nums.length;i ++) { if(i - 1 >= 0 && nums[i] == nums[i - 1] && visit[i - 1] == 0) { continue; } if (visit[i] == 0) { visit[i] = 1; list.add(nums[i]); dfs(list, nums, visit); visit[i] = 0; list.remove(list.size() - 1); } } } } 组合 给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。 示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ] 解析:组合长度固定为k,不能有重复组合,所以我们规定顺序是从小到大进行组合,这样的话就不会有重复情况出现了。 class Solution { static List<List<Integer>> Alllist = new ArrayList<>(); public List<List<Integer>> combine(int n, int k) { Alllist.clear(); List<Integer> list = new ArrayList<>(); dfs(list, n, k, 1); return Alllist; } public void dfs(List<Integer> list, int n, int k, int cur) { if (list.size() == k) { Alllist.add(new ArrayList(list)); return; } for (int i = cur;i <= n;i ++) { list.add(i); dfs(list, n, k, i + 1); list.remove(list.size() - 1); } } } 39. 组合总和 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的数字可以无限制重复被选取。 说明: 所有数字(包括 target)都是正整数。 解集不能包含重复的组合。 示例 1: 输入: candidates = [2,3,6,7], target = 7, 所求解集为: [ [7], [2,2,3] ] 示例 2: 输入: candidates = [2,3,5], target = 8, 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ] 解析:由于组合中没有重复数字,并且每个数可以出现多次,所以我们依然可以按顺序进行组合,每次i取自己或比自己更大的数组下标即可。 class Solution { static List<List<Integer>> Alllist = new ArrayList<>(); public List<List<Integer>> combinationSum(int[] candidates, int target) { Alllist.clear(); List<Integer> list = new ArrayList<>(); Arrays.sort(candidates); dfs(list, candidates, 0, target, 0); return Alllist; } public void dfs(List<Integer> list, int [] candidates, int sum, int target, int cur) { if (sum == target) { Alllist.add(new ArrayList(list)); return; } for (int i = cur;i < candidates.length;i ++) { if (sum + candidates[i] <= target) { list.add(candidates[i]); dfs(list, candidates, sum + candidates[i], target, i); list.remove(list.size() - 1); } } } 40. 组合总和 II 给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用一次。 说明: 所有数字(包括目标数)都是正整数。 解集不能包含重复的组合。 示例 1: 输入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集为: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ] 示例 2: 输入: candidates = [2,5,2,1,2], target = 5, 所求解集为: [ [1,2,2], [5] ] 解析:本题类似排序的第二题,组合求和,但是组合不能重复,而数组中允许重复数字。 所以我们依然要过滤掉同一位置上的重复数字。 class Solution { static List<List<Integer>> Alllist = new ArrayList<>(); public List<List<Integer>> combinationSum2(int[] candidates, int target) { Alllist.clear(); List<Integer> list = new ArrayList<>(); int []visit = new int[candidates.length]; Arrays.sort(candidates); dfs(list, candidates, 0, target, 0, visit); return Alllist; } public void dfs(List<Integer> list, int [] candidates, int sum, int target, int cur, int[] visit) { if (sum == target) { Alllist.add(new ArrayList(list)); return; } for (int i = cur;i < candidates.length;i ++) { if (i - 1 >= 0 && candidates[i] == candidates[i - 1] && visit[i - 1] == 0) { continue; } if (sum + candidates[i] <= target) { visit[i] = 1; list.add(candidates[i]); dfs(list, candidates, sum + candidates[i], target, i + 1, visit); list.remove(list.size() - 1); visit[i] = 0; } } } } 216. 组合总和 III 找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。 说明: 所有数字都是正整数。 解集不能包含重复的组合。 示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]] 示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]] 解析:与前面类似,没啥难度 class Solution { public List<List<Integer>> combinationSum3(int k, int n) { Alllist.clear(); List<Integer> list = new ArrayList<>(); dfs(list, 0, n, k, 1); return Alllist; } static List<List<Integer>> Alllist = new ArrayList<>(); public void dfs(List<Integer> list, int sum, int n, int k, int cur) { if (sum == n && list.size() == k) { Alllist.add(new ArrayList(list)); return; } for (int i = cur;i <= 9;i ++) { if (sum + i <= n && list.size() < k) { list.add(i); dfs(list, sum + i, n, k, i + 1); list.remove(list.size() - 1); } } } } 78. 子集 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 说明:解集不能包含重复的子集。 示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ] 解析:本题要注意的是,数字不重复,所以我们可以按顺序来查找子集,并且递归结束条件是当cur = nums.length。时结束。 class Solution { List<List<Integer>> Alllist = new ArrayList<>(); public List<List<Integer>> subsets(int[] nums) { List<Integer> list = new ArrayList<>(); dfs(nums, 0, list); return Alllist; } public void dfs(int []nums, int cur, List<Integer> list) { if (cur <= nums.length) { Alllist.add(new ArrayList(list)); } else { return; } for (int i = cur;i < nums.length;i ++) { list.add(nums[i]); dfs(nums, i + 1, list); list.remove(list.size() - 1); } } } 90. 子集 II 给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 说明:解集不能包含重复的子集。 示例: 输入: [1,2,2] 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ] class Solution { List<List<Integer>> Alllist = new ArrayList<>(); public List<List<Integer>> subsetsWithDup(int[] nums) { List<Integer> list = new ArrayList<>(); Arrays.sort(nums); int []visit = new int[nums.length]; dfs(nums, 0, visit, list); return Alllist; } public void dfs(int []nums, int cur, int []visit, List<Integer> list) { if (cur <= nums.length) { Alllist.add(new ArrayList(list)); } else { return; } for (int i = cur;i < nums.length;i ++) { if (i > 0 && visit[i - 1] == 0 && nums[i] == nums[i - 1]) { continue; } if (visit[i] == 0) { visit[i] = 1; list.add(nums[i]); dfs(nums, i + 1,visit, list); list.remove(list.size() - 1); visit[i] = 0; } } } } 131. 分割回文串 给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。 返回 s 所有可能的分割方案。 示例: 输入: “aab” 输出: [ [“aa”,”b”], [“a”,”a”,”b”] ] 解析: 这题其实也类似,只不过比较巧妙一点。 1 要把字符串分割成回文字符串组合,那么我们要存的其实还是所有的字符串,所以返回条件还是当cur = s.length()。 2 字符串分割,首先在循环中遍历满足回文的第一个子串,在此基础上找下一个回文子串,达到了剪枝的目的。 class Solution { List<List<String>> allList = new ArrayList<>(); public List<List<String>> partition(String s) { List<String> list = new ArrayList<>(); dfs(s, 0, list); return allList; } public void dfs(String s, int cur, List<String> list) { if (cur == s.length()) { allList.add(new ArrayList(list)); return; } for (int i = cur;i < s.length();i ++) { String str = s.substring(cur, i + 1); if (legal(str)) { list.add(str); dfs(s, i + 1, list); list.remove(list.size() - 1); } } } public boolean legal(String s) { int i = 0,j = s.length() - 1; while (i < j) { if (s.charAt(i) == s.charAt(j)) { i ++; j --; }else { return false; } } return true; } } 动态规划 动态规划 递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。 第一大类:斐波那契数列型,选择n - 1或n - 2的一种 爬楼梯 70. Climbing Stairs (Easy) 题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。 定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。 dp[N] 即为所求。 考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。 public int climbStairs(int n) { if (n <= 2) { return n; } int pre2 = 1, pre1 = 2; for (int i = 2; i < n; i++) { int cur = pre1 + pre2; pre2 = pre1; pre1 = cur; } return pre1; } 198. 打家劫舍 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。 示例 1: 输入: [1,2,3,1] 输出: 4 解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。 解析:注意抢劫的时候,对于 [1,2,3,4],要么偷1,要么投2。 但是对于4来说,前面的偷法可以分为两种,要么偷1不偷2和3,要么偷2不偷3。 举个例子[100,2,3,4]偷4的时候,我们应该放弃2和3,直接偷100。同时我们不用再往后判断了,因为前面也考虑到了这种情况。 public static int rob(int[] nums) { if (nums == null || nums.length == 0) return 0; if (nums.length == 1)return nums[0]; if (nums.length == 2)return nums[0] > nums[1] ? nums[0] : nums[1]; int []dp = new int[nums.length + 1]; //dp代表最右只抢到第n家时的总钱数。 dp[1] = nums[0]; dp[2] = nums[0] > nums[1] ? nums[0] : nums[1]; for (int i = 3;i <= nums.length;i ++) { dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]); } return dp[nums.length]; } 213. 打家劫舍 II 你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。 示例 1: 输入: [2,3,2] 输出: 3 解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。 示例 2: 输入: [1,2,3,1] 输出: 4 解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。 解析:环形区域主要考虑两种情况,第一家要抢的话,最后一家一定不能抢,第一家不抢的话,最后一家可以抢也可以不抢。然后根据上一题的做法使用dp即可。 public static int rob(int[] nums) { if (nums == null || nums.length == 0) return 0; if (nums.length == 1)return nums[0]; if (nums.length == 2)return nums[0] > nums[1] ? nums[0] : nums[1]; int []dp = new int[nums.length + 1]; //dp代表最右只抢到第n家时的总钱数。 //如果抢了第一家 dp[1] = nums[0]; dp[2] = nums[0] > nums[1] ? nums[0] : nums[1]; for (int i = 3;i < nums.length;i ++) { dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]); } int max = dp[nums.length - 1]; //如果不抢第一家 dp[1] = 0; dp[2] = nums[1]; for (int i = 3;i <= nums.length;i ++) { dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]); } if (dp[nums.length] > max)max = dp[nums.length]; return max; } 母牛生产 程序员代码面试指南-P181 题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。 第 i 年成熟的牛的数量为: 信件错排 题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。 定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况: i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)*dp[i-2] 种错误装信方式。 i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (i-1)*dp[i-1] 种错误装信方式。 综上所述,错误装信数量方式数量为: dp[N] 即为所求。 第二大类:矩阵路径 求矩阵路径和,走法总数或者最短距离等 64. 最小路径和 给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 说明:每次只能向下或者向右移动一步。 示例: 输入: [ [1,3,1], [1,5,1], [4,2,1] ] 输出: 7 解释: 因为路径 1→3→1→1→1 的总和最小。 class Solution { public int minPathSum(int[][] grid) { int [][]dp = new int[grid.length][grid[0].length]; dp[0][0] = grid[0][0]; for (int i = 1;i < grid.length;i ++) { dp[i][0] = dp[i - 1][0] + grid[i][0]; } for (int i = 1;i < grid[0].length;i ++) { dp[0][i] = dp[0][i - 1] + grid[0][i]; } for (int i = 1;i < grid.length;i ++) { for (int j = 1;j < grid[0].length;j ++) { dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; } } return dp[grid.length - 1][grid[0].length - 1]; } } 62. 不同路径 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 问总共有多少条不同的路径? 例如,上图是一个7 x 3 的网格。有多少可能的路径? 说明:m 和 n 的值均不超过 100。 示例 1: 输入: m = 3, n = 2 输出: 3 解释: 从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向右 -> 向下 2. 向右 -> 向下 -> 向右 3. 向下 -> 向右 -> 向右 示例 2: 输入: m = 7, n = 3 输出: 28 class Solution { public int uniquePaths(int m, int n) { int [][]dp = new int[m][n]; for (int i = 0; i < m;i ++) { dp[i][0] = 1; } for (int i = 0; i < n;i ++) { dp[0][i] = 1; } for (int i = 1;i < m;i ++) { for (int j = 1;j < n;j ++) { dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; } } return dp[m - 1][n - 1]; } } 第三大类:数组区间 53. 最大子序和 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 示例: 输入: [-2,1,-3,4,-1,2,1,-5,4], 输出: 6 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 进阶: 如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。 解析:设置数组dp表示结尾为第i个数的最大子数组和是多少。然后方程是dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]) 最后遍历一下dp数组找到最大子序和即可, class Solution { public int maxSubArray(int[] nums) { if (nums.length == 0) { return 0; } if (nums.length == 1) { return nums[0]; } int []dp = new int[nums.length]; dp[0] = nums[0]; for (int i = 1;i < nums.length;i ++) { dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]); } int max = nums[0]; for (int i = 0;i < dp.length;i ++) { max = Math.max(max, dp[i]); } return max; } } 第四类:分割整数 343. 整数拆分 给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。 例如,给定 n = 2,返回1(2 = 1 + 1);给定 n = 10,返回36(10 = 3 + 3 + 4)。 注意:你可以假设 n 不小于2且不大于58。 参考答案: 解析:对于一个数来说,拆分包括两种情况,一种是把i拆成j和i - j,另一种是i拆成j与i - j这个数所能构成的组合。 所以方程就是dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j))); 由于dp代表的是数字n能够得到的最大乘积,为了满足上面这个式子,dp[2] = dp[1] * 1,所以dp[1] = 1; public int integerBreak(int n) { int[] dp = new int[n + 1]; dp[1] = 1; for (int i = 2; i <= n; i++) { for (int j = 1; j <= i - 1; j++) { dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j))); } } return dp[n]; } 完全平方数 给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。 示例 1: 输入: n = 12 输出: 3 解释: 12 = 4 + 4 + 4. 示例 2: 输入: n = 13 输出: 2 解释: 13 = 4 + 9. 解析:恕在下直言,有些DP的题目也是神仙题,确实不容易想到。dp代表的和为n的完全平方数所需的最少个数。 本题还可以用bfs来做,详细可参见上面的bfs部分。 dp[1] = 1。所以对于每个i,我们都要遍历所有小于等于它的平方数,以便找到所需个数最少的个数。 方程写作:对于每个数i有一个最小值dp[i] min = Math.min(min, dp[i - square] + 1); 最后dp[n]即为所求。 非常巧妙,值得学习和记忆。 public int numSquares(int n) { List<Integer> squareList = generateSquareList(n); int[] dp = new int[n + 1]; for (int i = 1; i <= n; i++) { int min = Integer.MAX_VALUE; for (int square : squareList) { if (square > i) { break; } min = Math.min(min, dp[i - square] + 1); } dp[i] = min; } return dp[n]; } private List<Integer> generateSquareList(int n) { List<Integer> squareList = new ArrayList<>(); int diff = 3; int square = 1; while (square <= n) { squareList.add(square); square += diff; diff += 2; } return squareList; } 第五类:最长递增子序列 最长递增子序列 已知一个序列 {S1, S2,…,Sn} ,取出若干数组成新的序列 {Si1, Si2,…, Sim},其中 i1、i2 … im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 子序列 。 如果在子序列中,当下标 ix > iy 时,Six > Siy,称子序列为原序列的一个 递增子序列 。 定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1, Si2,…,Sim},如果 im < n 并且 Sim < Sn ,此时 {Si1, Si2,…, Sim, Sn} 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。 因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1,即: 解析: dp数组代表以第i个元素作为序列结尾时的最大递增序列,由于之前的序列是可选的,所以我们遍历之前所有满足情况的点(也就是满足nums[i] > nums[j]点),找到前面最长的递增序列即可。 转移方程是 dp[i] = Math.max(dp[j] + 1, dp[i]); class Solution { public int lengthOfLIS(int[] nums) { if (nums == null || nums.length == 0)return 0; if (nums.length == 1)return 1; int []dp = new int[nums.length]; dp[0] = 1; for (int i = 1;i < nums.length;i ++) { for (int j = 0;j < i;j ++) { if (nums[i] > nums[j]) { dp[i] = Math.max(dp[j] + 1, dp[i]); }else { dp[i] = Math.max(dp[i], 1); } } } int max = 0; for (int i = 0;i < dp.length;i ++) { max = Math.max(max, dp[i]); } return max; } } 第六类:最长公共子序列 最长公共子序列 对于两个子序列 S1 和 S2,找出它们最长的公共子序列。 定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况: 当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1 ,即 dp[i][j] = dp[i-1][j-1] + 1。 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,与 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。 综上,最长公共子序列的状态转移方程为: if (nums1[i - 1] == nums2[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } 对于长度为 N 的序列 S1 和 长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。 与最长递增子序列相比,最长公共子序列有以下不同点: 针对的是两个序列,求它们的最长公共子序列。 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j 。 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。 public int lengthOfLCS(int[] nums1, int[] nums2) { int n1 = nums1.length, n2 = nums2.length; int[][] dp = new int[n1 + 1][n2 + 1]; for (int i = 1; i <= n1; i++) { for (int j = 1; j <= n2; j++) { if (nums1[i - 1] == nums2[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } } return dp[n1][n2]; } 第七类:01背包 0-1 背包 有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。 定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论: 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。 第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。 综上,0-1 背包的状态转移方程为: public int knapsack(int W, int N, int[] weights, int[] values) { int[][] dp = new int[N + 1][W + 1]; for (int i = 1; i <= N; i++) { int w = weights[i - 1], v = values[i - 1]; for (int j = 1; j <= W; j++) { if (j >= w) { dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v); } else { dp[i][j] = dp[i - 1][j]; } } } return dp[N][W]; } 在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅由前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时, 因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],以防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。 public int knapsack(int W, int N, int[] weights, int[] values) { int[] dp = new int[W + 1]; for (int i = 1; i <= N; i++) { int w = weights[i - 1], v = values[i - 1]; for (int j = W; j >= 1; j–) { if (j >= w) { dp[j] = Math.max(dp[j], dp[j - w] + v); } } } return dp[W]; } 无法使用贪心算法的解释 0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。考虑下面的物品和一个容量为 5 的背包,如果先添加物品 0 再添加物品 1,那么只能存放的价值为 16,浪费了大小为 2 的空间。最优的方式是存放物品 1 和物品 2,价值为 22. id w v v/w 0 1 6 6 1 2 10 5 2 3 12 4 变种 完全背包:物品数量为无限个 多重背包:物品数量有限制 多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制 其它:物品之间相互约束或者依赖 划分数组为和相等的两部分 Partition Equal Subset Sum (Medium) Input: [1, 5, 11, 5] Output: true Explanation: The array can be partitioned as [1, 5, 5] and [11]. 可以看成一个背包大小为 sum/2 的 0-1 背包问题。 public boolean canPartition(int[] nums) { int sum = computeArraySum(nums); if (sum % 2 != 0) { return false; } int W = sum / 2; boolean[] dp = new boolean[W + 1]; dp[0] = true; Arrays.sort(nums); for (int num : nums) { // 0-1 背包一个物品只能用一次 for (int i = W; i >= num; i--) { // 从后往前,先计算 dp[i] 再计算 dp[i-num] dp[i] = dp[i] || dp[i - num]; } } return dp[W]; } private int computeArraySum(int[] nums) { int sum = 0; for (int num : nums) { sum += num; } return sum; } 找零钱的方法数 Coin Change (Medium) Example 1: coins = [1, 2, 5], amount = 11 return 3 (11 = 5 + 5 + 1) Example 2: coins = [2], amount = 3 return -1. 题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。 物品:硬币 物品大小:面额 物品价值:数量 因为硬币可以重复使用,因此这是一个完全背包问题。 public int coinChange(int[] coins, int amount) { if (coins == null || coins.length == 0) { return 0; } int[] minimum = new int[amount + 1]; Arrays.fill(minimum, amount + 1); minimum[0] = 0; Arrays.sort(coins); for (int i = 1; i <= amount; i++) { for (int j = 0; j < coins.length && coins[j] <= i; j++) { minimum[i] = Math.min(minimum[i], minimum[i - coins[j]] + 1); } } return minimum[amount] > amount ? -1 : minimum[amount]; } 第八类:股票买卖 只能进行 k 次的股票交易 Best Time to Buy and Sell Stock IV (Hard) public int maxProfit(int k, int[] prices) { int n = prices.length; if (k >= n / 2) { // 这种情况下该问题退化为普通的股票交易问题 int maxProfit = 0; for (int i = 1; i < n; i++) { if (prices[i] > prices[i - 1]) { maxProfit += prices[i] - prices[i - 1]; } } return maxProfit; } int[][] maxProfit = new int[k + 1][n]; for (int i = 1; i <= k; i++) { int localMax = maxProfit[i - 1][0] - prices[0]; for (int j = 1; j < n; j++) { maxProfit[i][j] = Math.max(maxProfit[i][j - 1], prices[j] + localMax); localMax = Math.max(localMax, maxProfit[i - 1][j] - prices[j]); } } return maxProfit[k][n - 1]; } 只能进行两次的股票交易 Best Time to Buy and Sell Stock III (Hard) public int maxProfit(int[] prices) { int firstBuy = Integer.MIN_VALUE, firstSell = 0; int secondBuy = Integer.MIN_VALUE, secondSell = 0; for (int curPrice : prices) { if (firstBuy < -curPrice) { firstBuy = -curPrice; } if (firstSell < firstBuy + curPrice) { firstSell = firstBuy + curPrice; } if (secondBuy < firstSell - curPrice) { secondBuy = firstSell - curPrice; } if (secondSell < secondBuy + curPrice) { secondSell = secondBuy + curPrice; } } return secondSell; } 需要冷却期的股票交易 Best Time to Buy and Sell Stock with Cooldown(Medium) 题目描述:交易之后需要有一天的冷却时间。 public int maxProfit(int[] prices) { if (prices == null || prices.length == 0) { return 0; } int N = prices.length; int[] buy = new int[N]; int[] s1 = new int[N]; int[] sell = new int[N]; int[] s2 = new int[N]; s1[0] = buy[0] = -prices[0]; sell[0] = s2[0] = 0; for (int i = 1; i < N; i++) { buy[i] = s2[i - 1] - prices[i]; s1[i] = Math.max(buy[i - 1], s1[i - 1]); sell[i] = Math.max(buy[i - 1], s1[i - 1]) + prices[i]; s2[i] = Math.max(s2[i - 1], sell[i - 1]); } return Math.max(sell[N - 1], s2[N - 1]); } 需要交易费用的股票交易 Best Time to Buy and Sell Stock with Transaction Fee (Medium) Input: prices = [1, 3, 2, 8, 4, 9], fee = 2 Output: 8 Explanation: The maximum profit can be achieved by: Buying at prices[0] = 1 Selling at prices[3] = 8 Buying at prices[4] = 4 Selling at prices[5] = 9 The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8. 题目描述:每交易一次,都要支付一定的费用。 public int maxProfit(int[] prices, int fee) { int N = prices.length; int[] buy = new int[N]; int[] s1 = new int[N]; int[] sell = new int[N]; int[] s2 = new int[N]; s1[0] = buy[0] = -prices[0]; sell[0] = s2[0] = 0; for (int i = 1; i < N; i++) { buy[i] = Math.max(sell[i - 1], s2[i - 1]) - prices[i]; s1[i] = Math.max(buy[i - 1], s1[i - 1]); sell[i] = Math.max(buy[i - 1], s1[i - 1]) - fee + prices[i]; s2[i] = Math.max(s2[i - 1], sell[i - 1]); } return Math.max(sell[N - 1], s2[N - 1]); } 买入和售出股票最大的收益 Best Time to Buy and Sell Stock (Easy) 题目描述:只进行一次交易。 只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益。 public int maxProfit(int[] prices) { int n = prices.length; if (n == 0) return 0; int soFarMin = prices[0]; int max = 0; for (int i = 1; i < n; i++) { if (soFarMin > prices[i]) soFarMin = prices[i]; else max = Math.max(max, prices[i] - soFarMin); } return max; } 第九类:字符串编辑 删除两个字符串的字符使它们相等 Delete Operation for Two Strings (Medium) Input: “sea”, “eat” Output: 2 Explanation: You need one step to make “sea” to “ea” and another step to make “eat” to “ea”. 可以转换为求两个字符串的最长公共子序列问题。 public int minDistance(String word1, String word2) { int m = word1.length(), n = word2.length(); int[][] dp = new int[m + 1][n + 1]; for (int i = 0; i <= m; i++) { for (int j = 0; j <= n; j++) { if (i == 0 || j == 0) { continue; } if (word1.charAt(i - 1) == word2.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); } } } return m + n - 2 * dp[m][n]; } 复制粘贴字符 2 Keys Keyboard (Medium) 题目描述:最开始只有一个字符 A,问需要多少次操作能够得到 n 个字符 A,每次操作可以复制当前所有的字符,或者粘贴。 Input: 3 Output: 3 Explanation: Intitally, we have one character ‘A’. In step 1, we use Copy All operation. In step 2, we use Paste operation to get ‘AA’. In step 3, we use Paste operation to get ‘AAA’. public int minSteps(int n) { if (n == 1) return 0; for (int i = 2; i <= Math.sqrt(n); i++) { if (n % i == 0) return i + minSteps(n / i); } return n; } public int minSteps(int n) { int[] dp = new int[n + 1]; int h = (int) Math.sqrt(n); for (int i = 2; i <= n; i++) { dp[i] = i; for (int j = 2; j <= h; j++) { if (i % j == 0) { dp[i] = dp[j] + dp[i / j]; break; } } } return dp[n]; } 位运算 数学 数学 素数 素数分解 每一个数都可以分解成素数的乘积,例如 84 = 22 * 31 * 50 * 71 * 110 * 130 * 170 * … 整除 令 x = 2m0 * 3m1 * 5m2 * 7m3 * 11m4 * … 令 y = 2n0 * 3n1 * 5n2 * 7n3 * 11n4 * … 如果 x 整除 y(y mod x == 0),则对于所有 i,mi <= ni。 最大公约数最小公倍数 x 和 y 的最大公约数为:gcd(x,y) = 2min(m0,n0) * 3min(m1,n1) * 5min(m2,n2) * … x 和 y 的最小公倍数为:lcm(x,y) = 2max(m0,n0) * 3max(m1,n1) * 5max(m2,n2) * … 分治 为运算表达式设计优先级 给定一个含有数字和运算符的字符串,为表达式添加括号,改变其运算优先级以求出不同的结果。你需要给出所有可能的组合的结果。有效的运算符号包含 +, - 以及 * 。 示例 1: 输入: “2-1-1” 输出: [0, 2] 解释: ((2-1)-1) = 0 (2-(1-1)) = 2 示例 2: 输入: “2*3-4*5” 输出: [-34, -14, -10, -10, 10] 解释: (2*(3-(4*5))) = -34 ((2*3)-(4*5)) = -14 ((2*(3-4))*5) = -10 (2*((3-4)*5)) = -10 (((2*3)-4)*5) = 10 Trie Trie,又称前缀树或字典树,用于判断字符串是否存在或者是否具有某种字符串前缀。 实现一个 Trie 208. Implement Trie (Prefix Tree) (Medium) class Trie { private class Node { Node[] childs = new Node[26]; boolean isLeaf; } private Node root = new Node(); public Trie() { } public void insert(String word) { insert(word, root); } private void insert(String word, Node node) { if (node == null) return; if (word.length() == 0) { node.isLeaf = true; return; } int index = indexForChar(word.charAt(0)); if (node.childs[index] == null) { node.childs[index] = new Node(); } insert(word.substring(1), node.childs[index]); } public boolean search(String word) { return search(word, root); } private boolean search(String word, Node node) { if (node == null) return false; if (word.length() == 0) return node.isLeaf; int index = indexForChar(word.charAt(0)); return search(word.substring(1), node.childs[index]); } public boolean startsWith(String prefix) { return startWith(prefix, root); } private boolean startWith(String prefix, Node node) { if (node == null) return false; if (prefix.length() == 0) return true; int index = indexForChar(prefix.charAt(0)); return startWith(prefix.substring(1), node.childs[index]); } private int indexForChar(char c) { return c - 'a'; } } 实现一个 Trie,用来求前缀和 677. Map Sum Pairs (Medium) Input: insert("apple", 3), Output: Null Input: sum("ap"), Output: 3 Input: insert("app", 2), Output: Null Input: sum("ap"), Output: 5 class MapSum { private class Node { Node[] child = new Node[26]; int value; } private Node root = new Node(); public MapSum() { } public void insert(String key, int val) { insert(key, root, val); } private void insert(String key, Node node, int val) { if (node == null) return; if (key.length() == 0) { node.value = val; return; } int index = indexForChar(key.charAt(0)); if (node.child[index] == null) { node.child[index] = new Node(); } insert(key.substring(1), node.child[index], val); } public int sum(String prefix) { return sum(prefix, root); } private int sum(String prefix, Node node) { if (node == null) return 0; if (prefix.length() != 0) { int index = indexForChar(prefix.charAt(0)); return sum(prefix.substring(1), node.child[index]); } int sum = node.value; for (Node child : node.child) { sum += sum(prefix, child); } return sum; } private int indexForChar(char c) { return c - 'a'; } } 图 二分图 如果可以用两种颜色对图中的节点进行着色,并且保证相邻的节点颜色不同,那么这个图就是二分图。 判断是否为二分图 785. Is Graph Bipartite? (Medium) Input: [[1,3], [0,2], [1,3], [0,2]] Output: true Explanation: The graph looks like this: 0----1 | | | | 3----2 We can divide the vertices into two groups: {0, 2} and {1, 3}. Example 2: Input: [[1,2,3], [0,2], [0,1,3], [0,2]] Output: false Explanation: The graph looks like this: 0----1 | \ | | \ | 3----2 We cannot find a way to divide the set of nodes into two independent subsets. public boolean isBipartite(int[][] graph) { int[] colors = new int[graph.length]; Arrays.fill(colors, -1); for (int i = 0; i 拓扑排序 常用于在具有先序关系的任务规划中。 课程安排的合法性 207. Course Schedule (Medium) 2, [[1,0]] return true 2, [[1,0],[0,1]] return false 题目描述:一个课程可能会先修课程,判断给定的先修课程规定是否合法。 本题不需要使用拓扑排序,只需要检测有向图是否存在环即可。 public boolean canFinish(int numCourses, int[][] prerequisites) { List[] graphic = new List[numCourses]; for (int i = 0; i 并查集 并查集可以动态地连通两个点,并且可以非常快速地判断两个点是否连通。 冗余连接 684. Redundant Connection (Medium) Input: [[1,2], [1,3], [2,3]] Output: [2,3] Explanation: The given undirected graph will be like this: 1 / \ 2 - 3 题目描述:有一系列的边连成的图,找出一条边,移除它之后该图能够成为一棵树。 public int[] findRedundantConnection(int[][] edges) { int N = edges.length; UF uf = new UF(N); for (int[] e : edges) { int u = e[0], v = e[1]; if (uf.connect(u, v)) { return e; } uf.union(u, v); } return new int[]{-1, -1}; } private class UF { private int[] id; UF(int N) { id = new int[N + 1]; for (int i = 0; i 位运算 1. 基本原理 0s 表示一串 0,1s 表示一串 1。 x ^ 0s = x x & 0s = 0 x | 0s = x x ^ 1s = ~x x & 1s = x x | 1s = 1s x ^ x = 0 x & x = x x | x = x 利用 x ^ 1s = ~x 的特点,可以将位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数。 利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask :00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位。 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1。 位与运算技巧: n&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110 100 ,减去 1 得到 10110**011**,这两个数相与得到 10110**000**。 n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1,对于二进制表示 10110 100 ,-n 得到 01001**100**,相与得到 00000**100**。 n-n&(~n+1) 去除 n 的位级表示中最高的那一位。 移位运算: >> n 为算术右移,相当于除以 2n; >>> n 为无符号右移,左边会补上 0。 << n 为算术左移,相当于乘以 2n。 2. mask 计算 要获取 111111111,将 0 取反即可,~0。 要得到只有第 i 位为 1 的 mask,将 1 向左移动 i-1 位即可,1<<(i-1) 。例如 1<<4 得到只有第 5 位为 1 的 mask :00010000。 要得到 1 到 i 位为 1 的 mask,1<<(i+1)-1 即可,例如将 1<<(4+1)-1 = 00010000-1 = 00001111。 要得到 1 到 i 位为 0 的 mask,只需将 1 到 i 位为 1 的 mask 取反,即 ~(1<<(i+1)-1)。 3. Java 中的位操作 static int Integer.bitCount(); // 统计 1 的数量 static int Integer.highestOneBit(); // 获得最高位 static String toBinaryString(int i); // 转换为二进制表示的字符串 统计两个数的二进制表示有多少位不同 461. Hamming Distance (Easy) Input: x = 1, y = 4 Output: 2 Explanation: 1 (0 0 0 1) 4 (0 1 0 0) ↑ ↑ The above arrows point to positions where the corresponding bits are different. 对两个数进行异或操作,位级表示不同的那一位为 1,统计有多少个 1 即可。 public int hammingDistance(int x, int y) { int z = x ^ y; int cnt = 0; while(z != 0) { if ((z & 1) == 1) cnt++; z = z >> 1; } return cnt; } 使用 z&(z-1) 去除 z 位级表示最低的那一位。 public int hammingDistance(int x, int y) { int z = x ^ y; int cnt = 0; while (z != 0) { z &= (z - 1); cnt++; } return cnt; } 可以使用 Integer.bitcount() 来统计 1 个的个数。 public int hammingDistance(int x, int y) { return Integer.bitCount(x ^ y); } 数组中唯一一个不重复的元素 136. Single Number (Easy) Input: [4,1,2,1,2] Output: 4 两个相同的数异或的结果为 0,对所有数进行异或操作,最后的结果就是单独出现的那个数。 public int singleNumber(int[] nums) { int ret = 0; for (int n : nums) ret = ret ^ n; return ret; } 找出数组中缺失的那个数 268. Missing Number (Easy) Input: [3,0,1] Output: 2 题目描述:数组元素在 0-n 之间,但是有一个数是缺失的,要求找到这个缺失的数。 public int missingNumber(int[] nums) { int ret = 0; for (int i = 0; i

优秀的个人博客,低调大师

CentOS目录结构超级详细版

使用linux也有一年多时间了 最近也是一直在维护网站系统主机 下面是linux目录结构说明 本人使用的是centos系统,很久没有发表博文了 近期会整理自己所用所了解知识点,发表linux相关的文章,记录自己的linux点点滴滴。 linux 目录结构 /:根目录,一般根目录下只存放目录,不要存放文件,/etc、/bin、/dev、/lib、/sbin应该和根目录放置在一个分区中 /bin:/usr/bin:可执行二进制文件的目录,如常用的命令ls、tar、mv、cat等。 /boot:放置linux系统启动时用到的一些文件。/boot/vmlinuz为linux的内核文件,以及/boot/gurb。建议单独分区,分区大小100M即可 /dev:存放linux系统下的设备文件,访问该目录下某个文件,相当于访问某个设备,常用的是挂载光驱mount /dev/cdrom /mnt。 /etc:系统配置文件存放的目录,不建议在此目录下存放可执行文件,重要的配置文件有/etc/inittab、/etc/fstab、/etc/init.d、/etc/X11、/etc/sysconfig、/etc/xinetd.d修改配置文件之前记得备份。注:/etc/X11存放与x windows有关的设置。 /home:系统默认的用户家目录,新增用户账号时,用户的家目录都存放在此目录下,~表示当前用户的家目录,~test表示用户test的家目录。建议单独分区,并设置较大的磁盘空间,方便用户存放数据 /lib:/usr/lib:/usr/local/lib:系统使用的函数库的目录,程序在执行过程中,需要调用一些额外的参数时需要函数库的协助,比较重要的目录为/lib/modules。 /lost+fount:系统异常产生错误时,会将一些遗失的片段放置于此目录下,通常这个目录会自动出现在装置目录下。如加载硬盘于/disk 中,此目录下就会自动产生目录/disk/lost+found /mnt:/media:光盘默认挂载点,通常光盘挂载于/mnt/cdrom下,也不一定,可以选择任意位置进行挂载。 /opt:给主机额外安装软件所摆放的目录。如:FC4使用的Fedora 社群开发软件,如果想要自行安装新的KDE 桌面软件,可以将该软件安装在该目录下。以前的 Linux 系统中,习惯放置在 /usr/local 目录下 /proc:此目录的数据都在内存中,如系统核心,外部设备,网络状态,由于数据都存放于内存中,所以不占用磁盘空间,比较重要的目录有/proc/cpuinfo、/proc/interrupts、/proc/dma、/proc/ioports、/proc/net/*等 /root:系统管理员root的家目录,系统第一个启动的分区为/,所以最好将/root和/放置在一个分区下。 /sbin:/usr/sbin:/usr/local/sbin:放置系统管理员使用的可执行命令,如fdisk、shutdown、mount等。与/bin不同的是,这几个目录是给系统管理员root使用的命令,一般用户只能"查看"而不能设置和使用。 /tmp:一般用户或正在执行的程序临时存放文件的目录,任何人都可以访问,重要数据不可放置在此目录下 /srv:服务启动之后需要访问的数据目录,如www服务需要访问的网页数据存放在/srv/www内 /usr:应用程序存放目录,/usr/bin存放应用程序,/usr/share存放共享数据,/usr/lib存放不能直接运行的,却是许多程序运行所必需的一些函数库文件。/usr/local:存放软件升级包。/usr/share/doc:系统说明文件存放目录。/usr/share/man:程序说明文件存放目录,使用 man ls时会查询/usr/share/man/man1/ls.1.gz的内容建议单独分区,设置较大的磁盘空间 /var:放置系统执行过程中经常变化的文件,如随时更改的日志文件/var/log,/var/log/message:所有的登录文件存放目录,/var/spool/mail:邮件存放的目录,/var/run:程序或服务启动 后,其PID存放在该目录下。建议单独分区,设置较大的磁盘空间 ------------------------------------------ /dev:目录 dev是设备(device)的英文缩写。/dev这个目录对所有的用户都十分重要。因为在这个目录中包含了所有Linux系统中使用的外部设备。但是这里并不是放的外部设备的驱动程序,这一点和 windows,dos操作系统不一样。它实际上是一个访问这些外部设备的端口。我们可以非常方便地去访问这些外部设备,和访问一个文件,一个目录没有任何区别。 Linux沿袭Unix的风格,将所有设备认成是一个文件。 设备文件分为两种:块设备文件(b)和字符设备文件(c) 设备文件一般存放在/dev目录下,对常见设备文件作如下说明: /dev/hd[a-t]:IDE设备 /dev/sd[a-z]:SCSI设备 /dev/fd[0-7]:标准软驱 /dev/md[0-31]:软raid设备 /dev/loop[0-7]:本地回环设备 /dev/ram[0-15]:内存 /dev/null:无限数据接收设备,相当于黑洞 /dev/zero:无限零资源 /dev/tty[0-63]:虚拟终端 /dev/ttyS[0-3]:串口 /dev/lp[0-3]:并口 /dev/console:控制台 /dev/fb[0-31]:framebuffer /dev/cdrom => /dev/hdc /dev/modem => /dev/ttyS[0-9] /dev/pilot => /dev/ttyS[0-9] /dev/random:随机数设备 /dev/urandom:随机数设备 (PS:随机数设备,后面我会再写篇博客总结一下) /dev目录下的节点是怎么创建的? devf或者udev会自动帮你创建得。 kobject是sysfs文件系统的基础,udev通过监测、检测sysfs来获取新创建的设备的。 ------------------------------------------ /etc:目录 包含很多文件.许多网络配置文件也在/etc 中. /etc/rc or /etc/rc.d or /etc/rc*.d 启动、或改变运行级时运行的scripts或scripts的目录. /etc/passwd 用户数据库,其中的域给出了用户名、真实姓名、家目录、加密的口令和用户的其他信息. /etc/fstab 启动时mount -a命令(在/etc/rc 或等效的启动文件中)自动mount的文件系统列表. Linux下,也包括用swapon -a启用的swap区的信息. /etc/group 类似/etc/passwd ,但说明的不是用户而是组. /etc/inittab init 的配置文件. /etc/issue getty 在登录提示符前的输出信息.通常包括系统的一段短说明或欢迎信息.内容由系统管理员确定. /etc/motd Message Of The Day,成功登录后自动输出.内容由系统管理员确定.经常用于通告信息,如计划关机时间的警告. /etc/mtab 当前安装的文件系统列表.由scripts初始化,并由mount 命令自动更新.需要一个当前安装的文件系统的列表时使用,例如df 命令. /etc/shadow 在安装了影子口令软件的系统上的影子口令文件.影子口令文件将/etc/passwd 文件中的加密口令移动到/etc/shadow 中,而后者只对root可读.这使破译口令更困难. /etc/login.defs login 命令的配置文件. /etc/printcap 类似/etc/termcap ,但针对打印机.语法不同. /etc/profile , /etc/csh.login , /etc/csh.cshrc 登录或启动时Bourne或C shells执行的文件.这允许系统管理员为所有用户建立全局缺省环境. /etc/securetty 确认安全终端,即哪个终端允许root登录.一般只列出虚拟控制台,这样就不可能(至少很困难)通过modem或网络闯入系统并得到超级用户特权. /etc/shells 列出可信任的shell.chsh 命令允许用户在本文件指定范围内改变登录shell.提供一台机器FTP服务的服务进程ftpd 检查用户shell是否列在 /etc/shells 文件中,如果不是将不允许该用户登录. /etc/sysconfig 网络配置相关目录 ------------------------------------------ /proc:目录 档名 文件内容 /proc/cmdline 加载 kernel 时所下达的相关参数!查阅此文件,可了解系统是如何启动的! /proc/cpuinfo 本机的 CPU 的相关资讯,包含时脉、类型与运算功能等 /proc/devices 这个文件记录了系统各个主要装置的主要装置代号,与 mknod 有关呢! /proc/filesystems 目前系统已经加载的文件系统罗! /proc/interrupts 目前系统上面的 IRQ 分配状态。 /proc/ioports 目前系统上面各个装置所配置的 I/O 位址。 /proc/kcore 这个就是内存的大小啦!好大对吧!但是不要读他啦! /proc/loadavg 还记得 top 以及 uptime 吧?没错!上头的三个平均数值就是记录在此! /proc/meminfo 使用 free 列出的内存资讯,嘿嘿!在这里也能够查阅到! /proc/modules 目前我们的 Linux 已经加载的模块列表,也可以想成是驱动程序啦! /proc/mounts 系统已经挂载的数据,就是用 mount 这个命令呼叫出来的数据啦! /proc/swaps 到底系统挂加载的内存在哪里?呵呵!使用掉的 partition 就记录在此啦! /proc/partitions 使用 fdisk -l 会出现目前所有的 partition 吧?在这个文件当中也有纪录喔! /proc/pci 在 PCI 汇流排上面,每个装置的详细情况!可用 lspci 来查阅! /proc/uptime 就是用 uptime 的时候,会出现的资讯啦! /proc/version 核心的版本,就是用 uname -a 显示的内容啦! /proc/bus/* 一些汇流排的装置,还有 U盘 的装置也记录在此喔! ------------------------------------------ /usr:目录 /usr 文件系统经常很大,因为所有程序安装在这里. /usr 里的所有文件一般来自Linux distribution;本地安装的程序和其他东西在/usr/local 下.这样可能在升级新版系统或新distribution时无须重新安装全部程序. /usr/etc 存放设置文件 /usr/games 存放游戏和教学文件 /usr/include 存放C开发工具的头文件 /usr/share 存放结构独立的数据 /usr/bin 几乎所有用户命令.有些命令在/bin 或/usr/local/bin 中. /usr/sbin 根文件系统不必要的系统管理命令,例如多数服务程序. /usr/share/man , /usr/share/info , /usr/share/doc 手册页、GNU信息文档和各种其他文档文件. /usr/include C编程语言的头文件.为了一致性这实际上应该在/usr/lib 下,但传统上支持这个名字. /usr/lib 程序或子系统的不变的数据文件,包括一些site-wide配置文件.名字lib来源于库(library); 编程的原始库存在/usr/lib 里. /usr/local 本地安装的软件和其他文件放在这里. /usr/src 存放程序的源代码 ------------------------------------------ /var:目录 /var 包括系统一般运行时要改变的数据.每个系统是特定的,即不通过网络与其他计算机共享. /var/catman 当要求格式化时的man页的cache.man页的源文件一般存在/usr/man/man* 中;有些man页可能有预格式化的版本,存在/usr/man/cat* 中.而其他的man页在第一次看时需要格式化,格式化完的版本存在/var/man 中,这样其他人再看相同的页时就无须等待格式化了. (/var/catman 经常被清除,就象清除临时目录一样.) /var/lib 系统正常运行时要改变的文件. /var/local /usr/local 中安装的程序的可变数据(即系统管理员安装的程序).注意,如果必要,即使本地安装的程序也会使用其他/var 目录,例如/var/lock . /var/lock 锁定文件.许多程序遵循在/var/lock 中产生一个锁定文件的约定,以支持他们正在使用某个特定的设备或文件.其他程序注意到这个锁定文件,将不试图使用这个设备或文件. /var/log 各种程序的Log文件,特别是login (/var/log/wtmp log所有到系统的登录和注销) 和syslog (/var/log/messages 里存储所有核心和系统程序信息. /var/log 里的文件经常不确定地增长,应该定期清除. /var/run 保存到下次引导前有效的关于系统的信息文件.例如, /var/run/utmp 包含当前登录的用户的信息. /var/spool mail, news, 打印队列和其他队列工作的目录.每个不同的spool在/var/spool 下有自己的子目录,例如,用户的邮箱在/var/spool/mail 中. /var/tmp 比/tmp 允许的大或需要存在较长时间的临时文件. (虽然系统管理员可能不允许/var/tmp 有很旧的文件.) ------------------------------------------ 比较重要的目录 在 Linux 系统中,有几个目录是特别需要注意的,以下提供几个需要注意的目录,以及预设相关的用途: /etc:这个目录相当重要,如前所述,你的开机与系统数据文件均在这个目录之下,因此当这个目录被破坏,那你的系统大概也就差不多该死掉了!而在往后的文件中,你会发现我们常常使用这个目录下的 /etc/rc.d/init.d 这个子目录,因为这个 init.d 子目录是开启一些 Linux 系统服务的 scripts (可以想成是批次檔 )的地方。而在 /etc/rc.d/rc.local 这个文件是开机的执行档。 /bin, /sbin, /usr/bin, /usr/sbin:这是系统预设的执行文件的放置目录,例如 root 常常使用的 userconf, netconf, perl, gcc, c++ 等等的数据都放在这几个目录中,所以如果你在提示字符下找不到某个执行档时,可以在这四个目录中查一查!其中, /bin, /usr/bin 是给系统使用者使用的指令,而 /sbin, /usr/sbin 则是给系统管理员使用的指令! /usr/local:这是系统预设的让你安装你后来升级的套件的目录。例如,当你发现有更新的 Web 套件(如 Apache )可以安装,而你又不想以 rpm 的方式升级你的套件,则你可以将 apache 这个套件安装在 /usr/local 底下。安装在这里有个好处,因为目前大家的系统都是差不多的,所以如果你的系统要让别人接管的话,也比较容易上手呀!也比较容易找的到数据喔!因此,如果你有需要的话,通常我都会将 /usr/local/bin 这个路径加到我的 path 中。 /home:这个是系统将有账号的人口的家目录设置的地方。 /var:这个路径就重要了!不论是登入、各类服务的问题发生时的记录、以及常态性的服务记录等等的记录目录,所以当你的系统有问题时,就需要来这个目录记录的文件数据中察看问题的所在啰!而 mail 的预设放置也是在这里,所以他是很重要的 /usr/share/man, /usr/local/man:这两个目录为放置各类套件说明档的地方,例如你如果执行 man man,则系统会自动去找这两个目录下的所有说明文件 文件种类: 谈完了文件格式之后,再来谈谈所谓的文件种类吧!我们在刚刚的属性介绍中提到了最前面的标志 ( d 或 - ) 可以代表目录或文件,那就是不同的文件种类啦!Linux 的文件种类主要有底下 这几种: 正规文件( regular file ):就是一般类型的文件,在由 ls –al 所显示出来的属性方面,第一个属性为 [ - ]。另外,依照文件的内容,又大略可以分为两种文件种类: 纯文字文件(ascii) :这是 Unix 系统中最多的一种啰,几乎只要我们可以用来做为设定的文件都属于这一种; 二进制文件(binary) :通常执行档除了 scripts (文字型批次文件)之外,就是这一种文件格式; 目录 (directory):就是目录!第一个属性为 [ d ]; 连结档 (link):就是类似 Windows 底下的快捷方式啦!第一个属性为 [ l ]; 设备档 (device):与系统周边相关的一些文件,通常都集中在 /dev 这个目录之下!通常又分为两种: 区块 (block) 设备档 :就是一些储存数据,以提供系统存取的接口设备,简单的说就是硬盘啦!例如你的一号硬盘的代码是 /dev/hda1 等等的文件啦!第一个属性为 [ b ]; 字符 (character) 设备档 :亦即是一些串行端口的接口设备,例如键盘、鼠标等等!第一个属性为 [ c ]。 Linux 的文件系统( inode ): 在 Linux 系统当中,每个文件不止有文件的内容数据,还包括文件的种种属性,例如:所属群组、所属使用者、能否执行、文 件建立时间、文件特殊属性等等。我们将每个文件的内容分为两个部分来储存,一个是文件的属性,另一个则是文件的内容。 为了应付这两个不同的咚咚,所以 ext2 规划出 inode 与 Block 来分别储存文件的属性( 放在 inode 当中 )与文件的内容( 放置在 Block area 当中 )。当我们要将一个 partition 格式化 ( format )为 ext2 时,就必须要指定 inode 与 Block 的大小才行,也就是说,当 partition 被格式化为 ext2 的文件系统时,他一定会有 inode table 与 block area 这两个区域。 Block 已经在前面说过了,他是数据储存的最小单位。那么 inode 是什么?!简单的说, Block 是记录『文件内容数据』的区域,至于 inode 则是记录『该文件的相关属性,以及文件内容 放置在哪一个 Block 之内』的信息。简单的说, inode 除了记录文件的属性外,同时还必须要具有指向( pointer )的功能,亦即指向文件内容放置的区块之中,好让操作系统可以正确的去 取得文件的内容啊 该文件的拥有者与群组(owner/group); 该文件的存取模式; 该文件的类型; 该文件的建立日期(ctime)、最近一次的读取时间(atime)、最近修改的时间 (mtime); 该文件的容量; 定义文件特性的旗标(flag),如 SetUID...; 该文件真正内容的指向 (pointer);

优秀的个人博客,低调大师

LAMP架构升级版--LNMMP

简介 LNMMP=Linux+Nginx+MySQL+Memcache+PHP; 利用Nginx的高性能特点做前端反向代理服务器,分发用户请求,并在能够利用缓存的地方使用Memcache缓存服务,以加速缓存效率,具体架构图如下; 具体部署 代理层:Nginx 编译安装Nginx 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 yum install gcc openssl-devel pcre-devel zlib-devel groupadd -r nginx useradd -r -g nginx -M nginx . /configure \ --prefix= /usr/local/nginx \ --sbin-path= /usr/sbin/nginx \ --conf-path= /etc/nginx/nginx .conf \ --error-log-path= /var/log/nginx/error .log \ --http-log-path= /var/log/nginx/access .log \ --pid-path= /var/run/nginx/nginx .pid \ --lock-path= /var/lock/nginx .lock \ --user=nginx \ --group=nginx \ --with-http_ssl_module \ --with-http_flv_module \ --with-http_stub_status_module \ --with-http_gzip_static_module \ --http-client-body-temp-path= /var/tmp/nginx/client/ \ --http-proxy-temp-path= /var/tmp/nginx/proxy/ \ --http-fastcgi-temp-path= /var/tmp/nginx/fcgi/ \ --http-uwsgi-temp-path= /var/tmp/nginx/uwsgi \ --http-scgi-temp-path= /var/tmp/nginx/scgi \ --with-pcre make && make install vi /etc/init .d /nginx # 编辑服务脚本 #!/bin/sh # # nginx - this script starts and stops the nginx daemon # # chkconfig: - 85 15 # description: Nginx is an HTTP(S) server, HTTP(S) reverse \ # proxy and IMAP/POP3 proxy server # processname: nginx # config: /etc/nginx/nginx.conf # config: /etc/sysconfig/nginx # pidfile: /var/run/nginx.pid # Source function library. . /etc/rc .d /init .d /functions # Source networking configuration. . /etc/sysconfig/network # Check that networking is up. [ "$NETWORKING" = "no" ] && exit 0 nginx= "/usr/sbin/nginx" prog=$( basename $nginx) NGINX_CONF_FILE= "/etc/nginx/nginx.conf" [ -f /etc/sysconfig/nginx ] && . /etc/sysconfig/nginx lockfile= /var/lock/subsys/nginx make_dirs() { # make required directories user=`nginx -V 2>&1 | grep "configure arguments:" | sed 's/[^*]*--user=\([^ ]*\).*/\1/g' -` options=`$nginx -V 2>&1 | grep 'configure arguments:' ` for opt in $options; do if [ ` echo $opt | grep '.*-temp-path' ` ]; then value=` echo $opt | cut -d "=" -f 2` if [ ! -d "$value" ]; then # echo "creating" $value mkdir -p $value && chown -R $user $value fi fi done } start() { [ -x $nginx ] || exit 5 [ -f $NGINX_CONF_FILE ] || exit 6 make_dirs echo -n $ "Starting $prog: " daemon $nginx -c $NGINX_CONF_FILE retval=$? echo [ $retval - eq 0 ] && touch $lockfile return $retval } stop() { echo -n $ "Stopping $prog: " killproc $prog -QUIT retval=$? echo [ $retval - eq 0 ] && rm -f $lockfile return $retval } restart() { configtest || return $? stop sleep 1 start } reload() { configtest || return $? echo -n $ "Reloading $prog: " killproc $nginx -HUP RETVAL=$? echo } force_reload() { restart } configtest() { $nginx -t -c $NGINX_CONF_FILE } rh_status() { status $prog } rh_status_q() { rh_status > /dev/null 2>&1 } case "$1" in start) rh_status_q && exit 0 $1 ;; stop) rh_status_q || exit 0 $1 ;; restart|configtest) $1 ;; reload) rh_status_q || exit 7 $1 ;; force-reload) force_reload ;; status) rh_status ;; condrestart|try-restart) rh_status_q || exit 0 ;; *) echo $ "Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload|configtest}" exit 2 esac 配置Nginx 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 upstream memcached { server 172.16.25.111:11211; keepalive 1024; } upstream webserver { server 172.16.25.112:80; server 172.16.25.113:80; } upstream phpserver { server 172.16.25.112:9000; server 172.16.25.113:9000; } server { listen 80; server_name xxrenzhe.lnmmp.com; access_log /var/log/nginx/lnmmp .access.log; error_log /var/log/nginx/lnmmp .errors.log notice; root /www/lnmmp .com; index index.php index.html; location / { set $memcached_key $request_uri; add_header X-mem-key $memcached_key; memcached_pass memcached; default_type test /html ; error_page 404 500 502 504 = @webnocache; } location @webnocache { rewrite ^(\/)?$ /index .php last; # 配置直接访问域名或IP地址时,重定向至index.php文件 rewrite ^/.*$ /set_mem .php?$request_uri last; # 将静态访问重定向至后端set_mem.php,以实现set memcache的功能 proxy_pass http: //webserver ; } location ~* \.php(\?.*)?$ { fastcgi_pass phpserver; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } 启动服务 1 2 3 4 chmod +x /etc/init .d /nginx chkconfig --add nginx chkconfig nginx on service nginx start 缓存层:Memcache+NFS 安装配置memcache 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 # 安装libevent tar xf libevent-2.0.21-stable. tar .gz cd libevent-2.0.21 . /configure --prefix= /usr/local/libevent make && make install echo "/usr/local/libevent/lib" > /etc/ld .so.conf.d /libevent .conf ldconfig # 安装memcache tar xf memcached-1.4.15. tar .gz cd memcached-1.4.15 . /configure --prefix= /usr/local/memcached --with-libevent= /usr/local/libevent make && make install # 编辑服务脚本 # 一次启动了2个memcache实例,一个用于Nginx,一个用于后端PHP服务 vi /etc/init .d /memcached #!/bin/bash # # Init file for memcached # # chkconfig: - 86 14 # description: Distributed memory caching daemon # # processname: memcached # config: /etc/sysconfig/memcached . /etc/rc .d /init .d /functions ## Default variables PORT1= "11211" PORT2= "11311" USER= "nobody" MAXCONN= "1024" CACHESIZE= "64" OPTIONS= "" RETVAL=0 prog= "/usr/local/memcached/bin/memcached" desc= "Distributed memory caching" lockfile1= "/var/lock/subsys/memcached_$PORT1" lockfile2= "/var/lock/subsys/memcached_$PORT2" start() { echo -n $ "Starting $desc (memcached): " daemon $prog -d -p $PORT1 -u $USER -c $MAXCONN -m $CACHESIZE -o "$OPTIONS" daemon $prog -d -p $PORT2 -u $USER -c $MAXCONN -m $CACHESIZE -o "$OPTIONS" RETVAL=$? [ $RETVAL - eq 0 ] && success && touch $lockfile1 && touch $lockfile2 || failure echo return $RETVAL } stop() { echo -n $ "Shutting down $desc (memcached): " killproc $prog RETVAL=$? [ $RETVAL - eq 0 ] && success && rm -f $lockfile1 && rm -f $lockfile2 | failure echo return $RETVAL } restart() { stop start } reload() { echo -n $ "Reloading $desc ($prog): " killproc $prog -HUP RETVAL=$? [ $RETVAL - eq 0 ] && success || failure echo return $RETVAL } case "$1" in start) start ;; stop) stop ;; restart) restart ;; condrestart) [ -e $lockfile1 ] && [ -e $lockfile1 ] && restart RETVAL=$? ;; reload) reload ;; status) status $prog RETVAL=$? ;; *) echo $ "Usage: $0 {start|stop|restart|condrestart|status}" RETVAL=1 esac exit $RETVAL 启动memcache服务 1 2 3 4 chmod +x /etc/init .d /memcached chkconfig --add memcached chkconfig memcached on service memcached start 安装配置NFS 1 2 3 4 5 6 7 8 yum -y install nfs-utils vi /etc/exports /www/lnmmp .com 172.16.0.0 /16 (rw,no_root_squash) exportfs -ar # 导出NFS共享目录 cd /www/lnmmp .com unzip phpwind_v9.0_utf8.zip mv phpwind_v9.0_utf8 /upload/ * . chmod -R 777 attachment conf data html res src themes windid Web层:Apache Apache的安装见博客“httpd-2.4编译安装及新特性详解”; PHP-fpm的安装见博客“LAMP-PHP-fpm服务器配置”; PHP加速器opcache的安装配置见博客“LAMP-各PHP加速器性能剖析”; 配置PHP支持memcache 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 tar xf memcache-2.2.5.tgz cd memcache-2.2.5 /usr/local/php/bin/phpize . /configure --with-php-config= /usr/local/php/bin/php-config -- enable -memcache make && make install # 上述安装完后会有类似这样的提示: Installing shared extensions: /usr/local/php/lib/php/extensions/no-debug-non-zts-20090626/ vi /usr/local/php/lib/php .ini # 配置memcache模块位置及php会话保存至memcache中 extension= /usr/local/php/lib/php/extensions/no-debug-non-zts-20090626/memcache .so session.save_handler = memcache session.save_path = "tcp://172.16.25.111:11311" # 自定义set memcache的程序 vi /www/lnmmp .com /set_mem .php <?php $fn = dirname (__FILE__).$_SERVER[ 'QUERY_STRING' ]; if (file_exists($fn)) { $data = file_get_contents($fn); $mem = new Memcache(); $mem->connect( "172.16.25.111" ,11311) or die( "Could not connect" ); $mem-> set ($_SERVER[ 'QUERY_STRING' ],$data,0,600) or die( "Failed to save data at the memcached server" ); header( 'Content-Length: ' .filesize($fn). "\r\n" ); header( 'Content-Type: text/html' . "\r\n" ); header( 'X-cache: MISS' . "\r\n" ); print "$data" ; } else { header( 'Location: http://xxrenzhe.lnmmp.com' . "\r\n" ); header( 'Content-Length: ' .filesize($fn). "\r\n" ); header( 'X-cache: ' .$fn. "\r\n" ); } ?> Apache配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 vi /etc/httpd/httpd .conf # DocumentRoot "/usr/local/apache/htdocs" # 注释掉此行 Include /etc/httpd/extra/httpd-vhosts .conf # 取消此行注释 vi /etc/httpd/extra/httpd-vhosts .conf <VirtualHost *:80> DocumentRoot "/www/lnmmp.com" ServerName xxrenzhe.lnmmp.com ErrorLog "logs/lnmmp.com-error_log" CustomLog "logs/lnmmp.com-access_log" common <Directory "/www/lnmmp.com" > Options Indexes FollowSymLinks AllowOverride None Require all granted < /Directory > < /VirtualHost > NFS目录挂载 1 mount -t nfs 172.16.25.111: /www/lnmmp .com /www/lnmmp .com 服务启动 1 2 service httpd start service php-fpm start 数据库层:Ameoba+MariaDB MariaDB的安装详见博客“MySQL初识-架构-安装-初始化-连接-管理工具-数据文件”; MariaDB的主从复制架构配置见博客“Maria10实现主从复制架构及SSL复制”; Ameoba安装见“http://docs.hexnova.com/amoeba/index.html”; Ameoba实现读写分离见“http://docs.hexnova.com/amoeba/rw-splitting.html” 测试验证 Nginx利用memcache缓存小静态文件测试效果 Nginx利用memcache缓存大静态文件测试效果 PHP利用memcache保存session数据测试效果 本文转自 xxrenzhe11 51CTO博客,原文链接:http://blog.51cto.com/xxrenzhe/1403784,如需转载请自行联系原作者

优秀的个人博客,低调大师

Openstack完整搭建系统整理版

Operating System How to login to Ubuntu(12.04) Server system Login as a normal user user: openstack password: password Obtain Root privilege sudo su - password: password Update Host configuration hostname controller echo "controller" > /etc/hostname cat > /etc/hosts << EOF 127.0.0.1 localhost 127.0.1.1 controller {put_eth0_ip_here} controller # The following lines are desirable for IPv6 capable hosts ::1 ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters EOF Configure the network Setup the network in Native OpenStack VM # NOTE: The current IP is statically assigned by our system. Please do not change it. vi /etc/network/interfaces # This file describes the network interfaces available on your system # and how to activate them. For more information, see interfaces(5). # The loopback network interface auto lo iface lo inet loopback auto eth0 iface eth0 inet static address 192.168.8.210 netmask 255.255.255.0 gateway 192.168.8.1 dns-nameservers 192.168.8.15 8.8.8.8 # 需在此配置DNS,不可直接修改/etc/resolv.conf文件 auto eth1 iface eth1 inet static address 192.168.8.211 netmask 255.255.255.0 auto eth2 iface eth2 inet static address 192.168.8.212 netmask 255.255.255.0 Restart the network service /etc/init.d/networking restart Enable IP forwarding # To permit IP packets pass through different networks,# the network card should be configured with routing capability. echo"net.ipv4.ip_forward = 1">>/etc/sysctl.conf sysctl -p Upgrade your system to the latest version Add software repository echo -en 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu precise-updates/havana main\n deb-src http://ubuntu-cloud.archive.canonical.com/ubuntu precise-updates/havana main' \ >> /etc/apt/sources.list.d/havana.list Upgrade the system apt-get update && apt-get dist-upgrade Install NTP Install the package apt-get install -y ntp Update /etc/ntp.conf file # Here we set ntp.ubuntu.com as the direct source of time.# You will also find that a local time source # is also provided in case of internet time service interruption. sed -i 's/server ntp.ubuntu.com/ \ server ntp.ubuntu.com \ server 127.127.1.0 \ fudge 127.127.1.0 stratum 10/g' /etc/ntp.conf Restart NTP service service ntp restart Set the OpenStack installation environment # Create the environment variables cat > /root/novarc << EOF export OS_TENANT_NAME=admin export OS_USERNAME=admin export OS_PASSWORD=password export MYSQL_PASS=password export SERVICE_PASSWORD=password export RABBIT_PASSWORD=password export FIXED_RANGE=10.0.0.0/24 export FLOATING_RANGE=$(/sbin/ifconfig eth0 | awk '/inet addr/ {print $2}' \ | cut -f2 -d ":" | awk -F "." '{print $1"."$2"."$3}').224/27 export OS_AUTH_URL="http://localhost:5000/v2.0/" export SERVICE_ENDPOINT="http://localhost:35357/v2.0" export SERVICE_TOKEN=stackinsider export MASTER="$(/sbin/ifconfig eth0 \ | awk '/inet addr/ {print $2}' | cut -f2 -d ":")" export LOCAL_IP="$(/sbin/ifconfig eth1 \ | awk '/inet addr/ {print $2}' | cut -f2 -d ":")" EOF # Update the global environment variables.cat/root/novarc >>/etc/profile source /etc/profile MySQL Server Setup the MySQL password for administrator cat << MYSQL_PRESEED | debconf-set-selections mysql-server-5.5 mysql-server/root_password password $MYSQL_PASS mysql-server-5.5 mysql-server/root_password_again password $MYSQL_PASS mysql-server-5.5 mysql-server/start_on_boot boolean true MYSQL_PRESEED Install the packages apt-get -y install mysql-server python-mysqldb curl Allow external connections # Bind MySQL service to all network interfaces. sed-i 's/127.0.0.1/0.0.0.0/g' /etc/mysql/my.cnf Restart MySQL service service mysql restart Create Databases, Users, Privileges for OpenStack mysql -uroot -p$MYSQL_PASS << EOF CREATE DATABASE nova; GRANT ALL PRIVILEGES ON nova.* TO 'nova'@'%' IDENTIFIED BY '$MYSQL_PASS'; GRANT ALL PRIVILEGES ON nova.* TO 'nova'@'localhost' IDENTIFIED BY '$MYSQL_PASS'; GRANT ALL PRIVILEGES ON nova.* TO 'nova'@'controller' IDENTIFIED BY '$MYSQL_PASS'; CREATE DATABASE glance; GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'%' IDENTIFIED BY '$MYSQL_PASS'; GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'localhost' IDENTIFIED BY '$MYSQL_PASS'; GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'controller' IDENTIFIED BY '$MYSQL_PASS'; CREATE DATABASE keystone; GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'%' IDENTIFIED BY '$MYSQL_PASS'; GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost' IDENTIFIED BY '$MYSQL_PASS'; GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'controller' IDENTIFIED BY '$MYSQL_PASS'; CREATE DATABASE cinder; GRANT ALL PRIVILEGES ON cinder.* TO 'cinder'@'%' IDENTIFIED BY '$MYSQL_PASS'; GRANT ALL PRIVILEGES ON cinder.* TO 'cinder'@'localhost' IDENTIFIED BY '$MYSQL_PASS'; GRANT ALL PRIVILEGES ON cinder.* TO 'cinder'@'controller' IDENTIFIED BY '$MYSQL_PASS'; CREATE DATABASE neutron; GRANT ALL PRIVILEGES ON neutron.* TO 'neutron'@'%' IDENTIFIED BY '$MYSQL_PASS'; GRANT ALL PRIVILEGES ON neutron.* TO 'neutron'@'localhost' IDENTIFIED BY '$MYSQL_PASS'; GRANT ALL PRIVILEGES ON neutron.* TO 'neutron'@'controller' IDENTIFIED BY '$MYSQL_PASS'; FLUSH PRIVILEGES; EOF Message Queue Server: RabbitMQ Install the packages # Install the messaging queue server. Typically it is RabbitMQ. apt-get-yinstallrabbitmq-server Change the default password rabbitmqctl change_password guest $RABBIT_PASSWORD OpenStack Identity Server: Keystone Install the packages apt-get -y install keystone Update /etc/keystone/keystone.conf sed -i -e " s/# admin_token = ADMIN/admin_token = $SERVICE_TOKEN/g; \ s/# bind_host = 0.0.0.0/bind_host = 0.0.0.0/g; \ s/# public_port = 5000/public_port = 5000/g; \ s/# admin_port = 35357/admin_port = 35357/g; \ s/# compute_port = 8774/compute_port = 8774/g; \ s/# verbose = True/verbose = True/g; \ s/# idle_timeout/idle_timeout/g" /etc/keystone/keystone.conf Update MySQL connection for Keyatone sed -i '/connection = .*/{s|sqlite:///.*|mysql://'"keystone"':'"$MYSQL_PASS"'@'"$MASTER"'/keystone|g}'\ /etc/keystone/keystone.conf Restart Keystone and Sync database service keystone restart keystone-manage db_sync Create users, tenants, services for OpenStack wget http://wiki.stackinsider.com/images/9/99/Ksdata.sh_.txt -O Ksdata.sh sed -i 's/quantum/neutron/g' Ksdata.sh sed -i 's/QUANTUM/NEUTRON/g' Ksdata.sh sh Ksdata.sh Create endpoints for OpenStack wget http://wiki.stackinsider.com/images/1/18/Ksendpoints.sh_.txt -O Ksendpoints.sh sed -i 's/quantum/neutron/g' Ksendpoints.sh sh Ksendpoints.sh OpenStack Image Server: Glance Install the packages apt-get -y install glance Update the credentials for Glance sed -i -e " s/%SERVICE_TENANT_NAME%/service/g; \ s/%SERVICE_USER%/glance/g; s/%SERVICE_PASSWORD%/$SERVICE_PASSWORD/g; \ " /etc/glance/glance-api.conf /etc/glance/glance-registry.conf Update MySQL connection for Glance sed -i '/sql_connection = .*/{s|sqlite:///.*|mysql://'"glance"':'"$MYSQL_PASS"'@'"$MASTER"'/glance|g}'\ /etc/glance/glance-registry.conf /etc/glance/glance-api.conf Setup notifier for Glance sed -i " s/notifier_strategy = noop/notifier_strategy = rabbit/g;\ s/rabbit_password = guest/rabbit_password = $RABBIT_PASSWORD/g;" \ /etc/glance/glance-api.conf Setup flavor for Glance cat << EOF >>/etc/glance/glance-api.conf flavor = keystone+cachemanagement EOF cat << EOF >>/etc/glance/glance-registry.conf flavor = keystone EOF Restart Glance services service glance-api restart service glance-registry restart Sync Glance database glance-manage db_sync Download Cirros image wget https://launchpad.net/cirros/trunk/0.3.0/+download/cirros-0.3.0-x86_64-disk.img Upload Cirros image to Glance glance add name=cirros-0.3.0-x86_64 is_public=truecontainer_format=bare \ disk_format=qcow2 </root/cirros-0.3.0-x86_64-disk.img Check the image # Check the image ID to confirm if Glance operates normally. glance index OpenStack Block Storage: Cinder Install the packages apt-get install -y cinder-api cinder-scheduler cinder-volume iscsitarget \ open-iscsi iscsitarget-dkms python-cinderclient Prepare the logical volumnes for Cinder You can manage Cinder via a pre-prepared logical volumes "cinder-volumes", the size of which is 1.2GB. fdisk -l pvcreate /dev/vdb vgcreate cinder-volumes /dev/vdb Volume group "cinder-volumes" successfully created iSCSI configuration Enable the iSCSI target service sed -i 's/false/true/g'/etc/default/iscsitarget Re-configure the kernel modules dpkg-reconfigure iscsitarget-dkms Restart iSCSI services service iscsitarget restart && service open-iscsi restart Update the configuration file of Cinder cat >/etc/cinder/cinder.conf <<EOF [DEFAULT] sql_connection = mysql://cinder:$MYSQL_PASS@$MASTER/cinder rootwrap_config = /etc/cinder/rootwrap.conf api_paste_confg = /etc/cinder/api-paste.ini iscsi_helper = tgtadm volume_name_template = volume-%s volume_group = cinder-volumes verbose = True auth_strategy = keystone state_path = /var/lib/cinder lock_path = /var/lock/cinder volumes_dir = /var/lib/cinder/volumes rabbit_password = $RABBIT_PASSWORD EOF Update the credentials of Cinder sed -i -e " s/%SERVICE_TENANT_NAME%/service/g; \ s/%SERVICE_USER%/cinder/g; s/%SERVICE_PASSWORD%/$SERVICE_PASSWORD/g; " \ /etc/cinder/api-paste.ini Synchronize the database of Cinder cinder-manage db sync Restart the services of Cinder service cinder-api restart service cinder-scheduler restart service cinder-volume restart OpenStack Network Server: Neutron Install the Open vSwitch apt-get install -y openvswitch-switch apt-get install module-assistant module-assistant auto-install openvswitch-datapath 若遇kernel-header错误,则执行以下命令,再执行上一步命令即可: ln -s /usr/src/linux-headers-`uname -r`/include/generated/uapi/linux/version.h /lib/modules/`uname -r`/build/include/linux/ Configure the network bridge Configure the bridge for internal communication ovs-vsctl add-br br-int Configure the bridge for external communication ovs-vsctl add-br br-eth2 # Enable external network access under nested Open vSwitchifconfig br-eth2 promisc up Bind eth2 to the external bridge ovs-vsctl add-port br-eth2 eth2 Update the external bridge configuration vim /etc/network/interfaces # Modify the corresponding configuration auto eth2 iface eth2 inet manual up ifconfig$IFACE 0.0.0.0 up up ip linkset$IFACE promisc on down ip linkset$IFACE promisc off down ifconfig$IFACE down auto br-eth2 iface br-eth2 inet static address {put_eth2_ip_here}netmask 255.255.255.0 up ip linkset$IFACE promisc on down ip linkset$IFACE promisc off Restart the network service /etc/init.d/networking restart Install Neutron apt-get -y install neutron-server python-cliff \ neutron-plugin-openvswitch-agent \ neutron-dhcp-agent python-pyparsing Configure the RabbitMQ sed -i -e " s/# auth_strategy/auth_strategy/g; \ s/# fake_rabbit/fake_rabbit/g; \ s/# rabbit_host = localhost/rabbit_host = $MASTER/g; \ s/# rabbit_password = guest/rabbit_password = $RABBIT_PASSWORD/g"/etc/neutron/neutron.conf Configure the Neutron Update the general configuration for Neutron cat << EOF >>/etc/neutron/plugins/openvswitch/ovs_neutron_plugin.ini [securitygroup] firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver [database]connection=mysql://neutron:$MYSQL_PASS@$MASTER/neutron [ovs] network_vlan_ranges = physnet1 bridge_mappings = physnet1:br-eth2 EOF Update the MySQL connection for Neutron sed -i '/connection = .*/{s|sqlite:///.*|mysql://'"neutron"':'"password"'@'"$MASTER"'/neutron|g}' \/etc/neutron/neutron.conf Update the metadata agent for Neutron sed -i -e " s/%SERVICE_TENANT_NAME%/service/g; s/%SERVICE_USER%/neutron/g; \ s/%SERVICE_PASSWORD%/$SERVICE_PASSWORD/g; "/etc/neutron/metadata_agent.ini Update the credentials for Neutron sed -i -e " s/%SERVICE_TENANT_NAME%/service/g; s/%SERVICE_USER%/neutron/g; \ s/%SERVICE_PASSWORD%/$SERVICE_PASSWORD/g; "/etc/neutron/neutron.conf Change the passphase of Neutron metadata agent sed -i -e " s/# metadata_proxy_shared_secret =/metadata_proxy_shared_secret \ = helloStackinsider/g; "/etc/neutron/metadata_agent.ini Change the passphase of Neutron dhcp agent sed -i -e " s/# interface_driver = neutron.agent.linux.interface.OVSInterfaceDriver/interface_driver = \ neutron.agent.linux.interface.OVSInterfaceDriver/g; "/etc/neutron/dhcp_agent.ini Restart the Neutron services cd /etc/init.d/; for i in $(ls neutron-*); do sudo service $i restart; done OpenStack Controller Server: Nova Controller Install the packages apt-get -y install nova-api nova-cert nova-common pm-utils nova-conductor \ nova-scheduler python-nova python-novaclient nova-consoleauth novnc nova-novncproxy Configure Nova Update the credential for Nova sed -i -e " s/127.0.0.1/$MASTER/g; s/%SERVICE_TENANT_NAME%/service/g; \ s/%SERVICE_USER%/nova/g; s/%SERVICE_PASSWORD%/$SERVICE_PASSWORD/g; " \ /etc/nova/api-paste.ini Update the general configuration: /etc/nova/nova.conf cat >/etc/nova/nova.conf <<EOF [DEFAULT] # MySQL Connection # sql_connection=mysql://nova:$MYSQL_PASS@$MASTER/nova # nova-scheduler # rabbit_host=$MASTER rabbit_password=$RABBIT_PASSWORD #scheduler_driver=nova.scheduler.simple.SimpleScheduler #compute_scheduler_driver=nova.scheduler.filter_scheduler.FilterScheduler compute_scheduler_driver=nova.scheduler.simple.SimpleScheduler # nova-api # cc_host=$MASTER auth_strategy=keystone s3_host=$MASTER ec2_host=$MASTER nova_url=http://$MASTER:8774/v1.1/ ec2_url=http://$MASTER:8773/services/Cloud keystone_ec2_url=http://$MASTER:5000/v2.0/ec2tokens api_paste_config=/etc/nova/api-paste.ini allow_admin_api=true use_deprecated_auth=false ec2_private_dns_show_ip=True dmz_cidr=169.254.169.254/32 ec2_dmz_host=169.254.169.254 metadata_host=$MASTER metadata_listen=0.0.0.0 enabled_apis=ec2,osapi_compute,metadata # Networking # network_api_class=nova.network.neutronv2.api.API neutron_url=http://$MASTER:9696 neutron_auth_strategy=keystone neutron_admin_tenant_name=service neutron_admin_username=neutron neutron_admin_password=$SERVICE_PASSWORD neutron_admin_auth_url=http://$MASTER:35357/v2.0 libvirt_vif_driver=nova.virt.libvirt.vif.LibvirtHybridOVSBridgeDriver linuxnet_interface_driver=nova.network.linux_net.LinuxOVSInterfaceDriver firewall_driver=nova.virt.libvirt.firewall.IptablesFirewallDriver #Metadata service_neutron_metadata_proxy = True neutron_metadata_proxy_shared_secret = helloStackinsider # Compute # compute_driver=libvirt.LibvirtDriver # Cinder # volume_api_class=nova.volume.cinder.API # Glance # glance_api_servers=$MASTER:9292 image_service=nova.image.glance.GlanceImageService # novnc # novnc_enable=true novncproxy_base_url=http://$MASTER:6080/vnc_auto.html vncserver_proxyclient_address=$MASTER vncserver_listen=$MASTER # Misc # logdir=/var/log/nova state_path=/var/lib/nova lock_path=/var/lock/nova #root_helper=sudo nova-rootwrap /etc/nova/rootwrap.conf rootwrap_config=/etc/nova/rootwrap.conf verbose=true #verbose=false EOF Synchronize Nova database nova-manage db sync Restart Nova services cd /etc/init.d/; for i in $( ls nova-* ); do sudo service $i restart; done OpenStack Compute Server: Nova Compute Install the Hypervisor apt-get install -y kvm libvirt-bin pm-utils Setup Cgroup support for libvirt cat << EOF >>/etc/libvirt/qemu.conf cgroup_device_acl = ["/dev/null", "/dev/full", "/dev/zero","/dev/random", "/dev/urandom","/dev/ptmx", "/dev/kvm", "/dev/kqemu","/dev/rtc", "/dev/hpet","/dev/net/tun", ] EOF Allow Live Migration sed -i '/#listen_tls/s/#listen_tls/listen_tls/;/#listen_tcp/s/#listen_tcp/listen_tcp/;/#auth_tcp/s/#auth_tcp/auth_tcp/; /auth_tcp/s/sasl/none/' \/etc/libvirt/libvirtd.conf Listen on TCP sed -i '/env libvirtd_opts/s/-d/-d -l/'/etc/init/libvirt-bin.conf sed -i '/libvirtd_opts/s/-d/-d -l/'/etc/default/libvirt-bin Restart libvirt-bin service service libvirt-bin restart Install the Nova Compute apt-get -yinstallnova-compute-kvm Modify the libvirt_type vi /etc/nova/nova-compute.conf libvirt_type=kvm 修改为 libvirt_type=qemu Restart Nova services cd /etc/init.d/; for i in $( ls nova-* ); do sudo service $i restart; done OpenStack Dashboard: Horizon Install Horizon apt-get -y install apache2 libapache2-mod-wsgi openstack-dashboard memcached python-memcache Prepare Tenant Network Create a bash script for preparation vi /root/prepare_network.sh #!/bin/bash # Create Tenant and User # tenant=TenantA user=UserA usermail=usera@stackinsider.com role=Member if keystone tenant-list | grep -q $tenant;then echo "Tenant $tenant existed!" else tenant_id=`keystone tenant-create --name $tenant | awk '/id/{print $4}'` fi if keystone user-list | grep -q $user;then echo "User $user existed!" else keystone user-create --name=$user --pass=password --tenant-id $tenant_id --email=$usermail fi keystone user-role-add --tenant $tenant --user $user --role $role # Create virtual router and sub-network # neutron net-create --tenant-id ${tenant_id} sharednet1 --shared --provider:network_type flat \ --provider:physical_network physnet1 neutron subnet-create --tenant-id ${tenant_id} sharednet1 192.168.100.0/24 --no-gateway \ --allocation-pool start=192.168.100.150,end=192.168.100.200 # 新建子网,配置共享网段 Execute the bash script to create network interconnection bash prepare_network.sh Set up the default security group rules # Obtain TenantA's default security group ID neutron --os-tenant-name TenantA --os-username UserA --os-password password --os-auth-url=http://localhost:5000/v2.0 security-group-list # Enable ICMP and TCP ports neutron security-group-rule-create --protocol icmp --direction ingress {TenantA security group ID} neutron security-group-rule-create --protocol icmp --direction egress {TenantA security group ID} neutron security-group-rule-create --protocol tcp --direction egress --port-range-min 1 --port-range-max 65535 {TenantA security group ID} neutron security-group-rule-create --protocol tcp --direction ingress --port-range-min 1 --port-range-max65535{TenantA security group ID} Start a VM Check the Ubuntu image # Check the image ID to confirm if Glance operates normally. glance index Generate ssh key and Upload it to Nova # Generate ssh key ssh-keygen # Upload ssh pub key to nova nova keypair-add key01 --pub-key~ /.ssh /id_rsa.pub Launch a VM nova --os-tenant-name TenantA --os-username UserA --os-password password --os-auth-url=http://localhost:5000/v2.0 boot --flavor 1 --image{the cirros ID from Glance} --security_group default --key-name key01 vm001 Check the VM status # Check your VM status nova --os-tenant-name TenantA --os-username UserA --os-password password \ --os-auth-url=http://localhost:5000/v2.0 list Access the VM instance using its flat IP # Obtain the VM's fixed IP nova --os-tenant-name TenantA --os-username UserA --os-password password \ --os-auth-url=http://localhost:5000/v2.0 list # You can find its fixed IP in the "Networks" section: sharednet1={flat IP}.# SSH to your VM when your VM is ACTIVEssh cirros@{put_flat_ip_here} Access the Dashboard http://controller/horizon(controller为主机名) Questions 执行“neutron--os-tenant-nameTenantA--os-usernameUserA--os-passwordpassword--os-auth-url=http://localhost:5000/v2.0 security-group-list”出现错误: “404 Not Found.The resource could not be found.” 解决: vi /etc/neutron/plugins/openvswitch/ovs_neutron_plugin.ini [securitygroup] # Firewall driver for realizing neutron security group function. # firewall_driver = neutron.agent.firewall.NoopFirewallDriver firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver 然后重启neutron服务: cd /etc/init.d/; for i in $( ls neutron-* ); do sudo service $i restart; done 本文转自 xxrenzhe11 51CTO博客,原文链接:http://blog.51cto.com/xxrenzhe/1351626,如需转载请自行联系原作者

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册