题目描述(困难难度)

68. Text Justification - 图1

一个句子,和一个长度表示一行最长的长度,然后对齐文本,有下边几个规则。

  1. 同一个单词只能出现在一行中,不能拆分
  2. 一行如果只能放下一个单词,该单词放在最左边,然后空格补齐,例如 “acknowledgement#####”,这里只是我为了直观,# 表示空格,题目并没有要求。
  3. 一行如果有多个单词,最左边和最右边不能有空格,每个单词间隙尽量平均,如果无法平均,把剩余的空隙从左边开始分配。例如,”enough###to###explain##to”,3 个间隙,每个 2 个空格的话,剩下 2 个空格,从左边依次添加一个空格。
  4. 最后一行执行左对齐,单词间一个空格,末尾用空格补齐。

解法一

这道题关键就是理解题目,然后就是一些细节的把控了,我主要是下边的想法。

一行一行计算该行可以放多少个单词,然后计算单词间的空隙是多少,然后把它添加到结果中。

  1. public List<String> fullJustify(String[] words, int maxWidth) {
  2. List<String> ans = new ArrayList<>();
  3. //当前行单词已经占用的长度
  4. int currentLen = 0;
  5. //保存当前行的单词
  6. List<String> row = new ArrayList<>();
  7. //遍历每个单词
  8. for (int i = 0; i < words.length;) {
  9. //判断加入该单词是否超过最长长度
  10. //分了两种情况,一种情况是加入第一个单词,不需要多加 1
  11. //已经有单词的话,再加入单词,需要多加个空格,所以多加了 1
  12. if (currentLen == 0 && currentLen + words[i].length() <= maxWidth
  13. || currentLen > 0 && currentLen + 1 + words[i].length() <= maxWidth) {
  14. row.add(words[i]);
  15. if (currentLen == 0) {
  16. currentLen = currentLen + words[i].length();
  17. } else {
  18. currentLen = currentLen + 1 + words[i].length();
  19. }
  20. i++;
  21. //超过的最长长度,对 row 里边的单词进行处理
  22. } else {
  23. //计算有多少剩余,也就是总共的空格数,因为之前计算 currentLen 多算了一个空格,这里加回来
  24. int sub = maxWidth - currentLen + row.size() - 1;
  25. //如果只有一个单词,那么就直接单词加空格就可以
  26. if (row.size() == 1) {
  27. String blank = getStringBlank(sub);
  28. ans.add(row.get(0) + blank);
  29. } else {
  30. //用来保存当前行的结果
  31. StringBuilder temp = new StringBuilder();
  32. //将第一个单词加进来
  33. temp.append(row.get(0));
  34. //计算平均空格数
  35. int averageBlank = sub / (row.size() - 1);
  36. //如果除不尽,计算剩余空格数
  37. int missing = sub - averageBlank * (row.size() - 1);
  38. //前 missing 的空格数比平均空格数多 1
  39. String blank = getStringBlank(averageBlank + 1);
  40. int k = 1;
  41. for (int j = 0; j < missing; j++) {
  42. temp.append(blank + row.get(k));
  43. k++;
  44. }
  45. //剩下的空格数就是求得的平均空格数
  46. blank = getStringBlank(averageBlank);
  47. for (; k < row.size(); k++) {
  48. temp.append(blank + row.get(k));
  49. }
  50. //将当前结果加入
  51. ans.add(temp.toString());
  52. }
  53. //清空以及置零
  54. row = new ArrayList<>();
  55. currentLen = 0;
  56. }
  57. }
  58. //单独考虑最后一行,左对齐
  59. StringBuilder temp = new StringBuilder();
  60. temp.append(row.get(0));
  61. for (int i = 1; i < row.size(); i++) {
  62. temp.append(" " + row.get(i));
  63. }
  64. //剩余部分用空格补齐
  65. temp.append(getStringBlank(maxWidth - currentLen));
  66. //最后一行加入到结果中
  67. ans.add(temp.toString());
  68. return ans;
  69. }
  70. //得到 n 个空白
  71. private String getStringBlank(int n) {
  72. StringBuilder str = new StringBuilder();
  73. for (int i = 0; i < n; i++) {
  74. str.append(" ");
  75. }
  76. return str.toString();
  77. }

时间复杂度:

空间复杂度:

但是这个算法,在 leetcode 跑,速度只打败了 30% 多的人,1 ms。然后在 discuss 里转了一圈寻求原因,发现大家思路都是这样子,然后找了一个人的跑了下,链接

  1. public List<String> fullJustify(String[] words, int maxWidth) {
  2. int left = 0; List<String> result = new ArrayList<>();
  3. while (left < words.length) {
  4. int right = findRight(left, words, maxWidth);
  5. result.add(justify(left, right, words, maxWidth));
  6. left = right + 1;
  7. }
  8. return result;
  9. }
  10. //找到当前行最右边的单词下标
  11. private int findRight(int left, String[] words, int maxWidth) {
  12. int right = left;
  13. int sum = words[right++].length();
  14. while (right < words.length && (sum + 1 + words[right].length()) <= maxWidth)
  15. sum += 1 + words[right++].length();
  16. return right - 1;
  17. }
  18. //根据不同的情况添加不同的空格
  19. private String justify(int left, int right, String[] words, int maxWidth) {
  20. if (right - left == 0) return padResult(words[left], maxWidth);
  21. boolean isLastLine = right == words.length - 1;
  22. int numSpaces = right - left;
  23. int totalSpace = maxWidth - wordsLength(left, right, words);
  24. String space = isLastLine ? " " : blank(totalSpace / numSpaces);
  25. int remainder = isLastLine ? 0 : totalSpace % numSpaces;
  26. StringBuilder result = new StringBuilder();
  27. for (int i = left; i <= right; i++)
  28. result.append(words[i])
  29. .append(space)
  30. .append(remainder-- > 0 ? " " : "");
  31. return padResult(result.toString().trim(), maxWidth);
  32. }
  33. //当前单词的长度
  34. private int wordsLength(int left, int right, String[] words) {
  35. int wordsLength = 0;
  36. for (int i = left; i <= right; i++) wordsLength += words[i].length();
  37. return wordsLength;
  38. }
  39. private String padResult(String result, int maxWidth) {
  40. return result + blank(maxWidth - result.length());
  41. }
  42. private String blank(int length) {
  43. return new String(new char[length]).replace('\0', ' ');
  44. }

看了下,发现思想和自己也是一样的。但是这个速度却打败了 100% ,0 ms。考虑了下,差别应该在我的算法里使用了一个叫做 row 的 list 用来保存当前行的单词,用了很多 row.get ( index ),而上边的算法只记录了 left 和 right 下标,取单词直接用的 words 数组。然后尝试着在我之前的算法上改了一下,去掉 row,用两个变量 start 和 end 保存当前行的单词范围。主要是 ( end - start ) 代替了之前的 row.size ( ), words [ start + k ] 代替了之前的 row.get ( k )。

  1. public List<String> fullJustify2(String[] words, int maxWidth) {
  2. List<String> ans = new ArrayList<>();
  3. int currentLen = 0;
  4. int start = 0;
  5. int end = 0;
  6. for (int i = 0; i < words.length;) {
  7. //判断加入该单词是否超过最长长度
  8. //分了两种情况,一种情况是加入第一个单词,不需要多加 1
  9. //已经有单词的话,再加入单词,需要多加个空格,所以多加了 1
  10. if (currentLen == 0 && currentLen + words[i].length() <= maxWidth
  11. || currentLen > 0 && currentLen + 1 + words[i].length() <= maxWidth) {
  12. end++;
  13. if (currentLen == 0) {
  14. currentLen = currentLen + words[i].length();
  15. } else {
  16. currentLen = currentLen + 1 + words[i].length();
  17. }
  18. i++;
  19. } else {
  20. int sub = maxWidth - currentLen + (end - start) - 1;
  21. if (end - start == 1) {
  22. String blank = getStringBlank(sub);
  23. ans.add(words[start] + blank);
  24. } else {
  25. StringBuilder temp = new StringBuilder();
  26. temp.append(words[start]);
  27. int averageBlank = sub / ((end - start) - 1);
  28. //如果除不尽,计算剩余空格数
  29. int missing = sub - averageBlank * ((end - start) - 1);
  30. String blank = getStringBlank(averageBlank + 1);
  31. int k = 1;
  32. for (int j = 0; j < missing; j++) {
  33. temp.append(blank + words[start+k]);
  34. k++;
  35. }
  36. blank = getStringBlank(averageBlank);
  37. for (; k <(end - start); k++) {
  38. temp.append(blank + words[start+k]);
  39. }
  40. ans.add(temp.toString());
  41. }
  42. start = end;
  43. currentLen = 0;
  44. }
  45. }
  46. StringBuilder temp = new StringBuilder();
  47. temp.append(words[start]);
  48. for (int i = 1; i < (end - start); i++) {
  49. temp.append(" " + words[start+i]);
  50. }
  51. temp.append(getStringBlank(maxWidth - currentLen));
  52. ans.add(temp.toString());
  53. return ans;
  54. }
  55. //得到 n 个空白
  56. private String getStringBlank(int n) {
  57. StringBuilder str = new StringBuilder();
  58. for (int i = 0; i < n; i++) {
  59. str.append(" ");
  60. }
  61. return str.toString();
  62. }

果然,速度也到了打败 100%,0 ms。

充分说明 list 的读取还是没有数组的直接读取快呀,还有就是要向上边的作者学习,多封装几个函数,思路会更加清晰,代码也会简明。

windliang wechat

添加好友一起进步~

如果觉得有帮助的话,可以点击 这里 给一个 star 哦 ^^

如果想系统的学习数据结构和算法,强烈推荐一个我之前学过的课程,可以点击 这里 查看详情