一个句子,和一个长度表示一行最长的长度,然后对齐文本,有下边几个规则。
- 同一个单词只能出现在一行中,不能拆分
- 一行如果只能放下一个单词,该单词放在最左边,然后空格补齐,例如 "acknowledgement#####",这里只是我为了直观,# 表示空格,题目并没有要求。
- 一行如果有多个单词,最左边和最右边不能有空格,每个单词间隙尽量平均,如果无法平均,把剩余的空隙从左边开始分配。例如,"enough###to###explain##to",3 个间隙,每个 2 个空格的话,剩下 2 个空格,从左边依次添加一个空格。
- 最后一行执行左对齐,单词间一个空格,末尾用空格补齐。
这道题关键就是理解题目,然后就是一些细节的把控了,我主要是下边的想法。
一行一行计算该行可以放多少个单词,然后计算单词间的空隙是多少,然后把它添加到结果中。
public List<String> fullJustify(String[] words, int maxWidth) {
List<String> ans = new ArrayList<>();
//当前行单词已经占用的长度
int currentLen = 0;
//保存当前行的单词
List<String> row = new ArrayList<>();
//遍历每个单词
for (int i = 0; i < words.length;) {
//判断加入该单词是否超过最长长度
//分了两种情况,一种情况是加入第一个单词,不需要多加 1
//已经有单词的话,再加入单词,需要多加个空格,所以多加了 1
if (currentLen == 0 && currentLen + words[i].length() <= maxWidth
|| currentLen > 0 && currentLen + 1 + words[i].length() <= maxWidth) {
row.add(words[i]);
if (currentLen == 0) {
currentLen = currentLen + words[i].length();
} else {
currentLen = currentLen + 1 + words[i].length();
}
i++;
//超过的最长长度,对 row 里边的单词进行处理
} else {
//计算有多少剩余,也就是总共的空格数,因为之前计算 currentLen 多算了一个空格,这里加回来
int sub = maxWidth - currentLen + row.size() - 1;
//如果只有一个单词,那么就直接单词加空格就可以
if (row.size() == 1) {
String blank = getStringBlank(sub);
ans.add(row.get(0) + blank);
} else {
//用来保存当前行的结果
StringBuilder temp = new StringBuilder();
//将第一个单词加进来
temp.append(row.get(0));
//计算平均空格数
int averageBlank = sub / (row.size() - 1);
//如果除不尽,计算剩余空格数
int missing = sub - averageBlank * (row.size() - 1);
//前 missing 的空格数比平均空格数多 1
String blank = getStringBlank(averageBlank + 1);
int k = 1;
for (int j = 0; j < missing; j++) {
temp.append(blank + row.get(k));
k++;
}
//剩下的空格数就是求得的平均空格数
blank = getStringBlank(averageBlank);
for (; k < row.size(); k++) {
temp.append(blank + row.get(k));
}
//将当前结果加入
ans.add(temp.toString());
}
//清空以及置零
row = new ArrayList<>();
currentLen = 0;
}
}
//单独考虑最后一行,左对齐
StringBuilder temp = new StringBuilder();
temp.append(row.get(0));
for (int i = 1; i < row.size(); i++) {
temp.append(" " + row.get(i));
}
//剩余部分用空格补齐
temp.append(getStringBlank(maxWidth - currentLen));
//最后一行加入到结果中
ans.add(temp.toString());
return ans;
}
//得到 n 个空白
private String getStringBlank(int n) {
StringBuilder str = new StringBuilder();
for (int i = 0; i < n; i++) {
str.append(" ");
}
return str.toString();
}
时间复杂度:
空间复杂度:
但是这个算法,在 leetcode 跑,速度只打败了 30% 多的人,1 ms。然后在 discuss 里转了一圈寻求原因,发现大家思路都是这样子,然后找了一个人的跑了下,链接。
public List<String> fullJustify(String[] words, int maxWidth) {
int left = 0; List<String> result = new ArrayList<>();
while (left < words.length) {
int right = findRight(left, words, maxWidth);
result.add(justify(left, right, words, maxWidth));
left = right + 1;
}
return result;
}
//找到当前行最右边的单词下标
private int findRight(int left, String[] words, int maxWidth) {
int right = left;
int sum = words[right++].length();
while (right < words.length && (sum + 1 + words[right].length()) <= maxWidth)
sum += 1 + words[right++].length();
return right - 1;
}
//根据不同的情况添加不同的空格
private String justify(int left, int right, String[] words, int maxWidth) {
if (right - left == 0) return padResult(words[left], maxWidth);
boolean isLastLine = right == words.length - 1;
int numSpaces = right - left;
int totalSpace = maxWidth - wordsLength(left, right, words);
String space = isLastLine ? " " : blank(totalSpace / numSpaces);
int remainder = isLastLine ? 0 : totalSpace % numSpaces;
StringBuilder result = new StringBuilder();
for (int i = left; i <= right; i++)
result.append(words[i])
.append(space)
.append(remainder-- > 0 ? " " : "");
return padResult(result.toString().trim(), maxWidth);
}
//当前单词的长度
private int wordsLength(int left, int right, String[] words) {
int wordsLength = 0;
for (int i = left; i <= right; i++) wordsLength += words[i].length();
return wordsLength;
}
private String padResult(String result, int maxWidth) {
return result + blank(maxWidth - result.length());
}
private String blank(int length) {
return new String(new char[length]).replace('\0', ' ');
}
看了下,发现思想和自己也是一样的。但是这个速度却打败了 100% ,0 ms。考虑了下,差别应该在我的算法里使用了一个叫做 row 的 list 用来保存当前行的单词,用了很多 row.get ( index ),而上边的算法只记录了 left 和 right 下标,取单词直接用的 words 数组。然后尝试着在我之前的算法上改了一下,去掉 row,用两个变量 start 和 end 保存当前行的单词范围。主要是 ( end - start ) 代替了之前的 row.size ( ), words [ start + k ] 代替了之前的 row.get ( k )。
public List<String> fullJustify2(String[] words, int maxWidth) {
List<String> ans = new ArrayList<>();
int currentLen = 0;
int start = 0;
int end = 0;
for (int i = 0; i < words.length;) {
//判断加入该单词是否超过最长长度
//分了两种情况,一种情况是加入第一个单词,不需要多加 1
//已经有单词的话,再加入单词,需要多加个空格,所以多加了 1
if (currentLen == 0 && currentLen + words[i].length() <= maxWidth
|| currentLen > 0 && currentLen + 1 + words[i].length() <= maxWidth) {
end++;
if (currentLen == 0) {
currentLen = currentLen + words[i].length();
} else {
currentLen = currentLen + 1 + words[i].length();
}
i++;
} else {
int sub = maxWidth - currentLen + (end - start) - 1;
if (end - start == 1) {
String blank = getStringBlank(sub);
ans.add(words[start] + blank);
} else {
StringBuilder temp = new StringBuilder();
temp.append(words[start]);
int averageBlank = sub / ((end - start) - 1);
//如果除不尽,计算剩余空格数
int missing = sub - averageBlank * ((end - start) - 1);
String blank = getStringBlank(averageBlank + 1);
int k = 1;
for (int j = 0; j < missing; j++) {
temp.append(blank + words[start+k]);
k++;
}
blank = getStringBlank(averageBlank);
for (; k <(end - start); k++) {
temp.append(blank + words[start+k]);
}
ans.add(temp.toString());
}
start = end;
currentLen = 0;
}
}
StringBuilder temp = new StringBuilder();
temp.append(words[start]);
for (int i = 1; i < (end - start); i++) {
temp.append(" " + words[start+i]);
}
temp.append(getStringBlank(maxWidth - currentLen));
ans.add(temp.toString());
return ans;
}
//得到 n 个空白
private String getStringBlank(int n) {
StringBuilder str = new StringBuilder();
for (int i = 0; i < n; i++) {
str.append(" ");
}
return str.toString();
}
果然,速度也到了打败 100%,0 ms。
充分说明 list 的读取还是没有数组的直接读取快呀,还有就是要向上边的作者学习,多封装几个函数,思路会更加清晰,代码也会简明。