【问题标题】:Alibaba interview: print a sentence with min spaces阿里巴巴采访:用最小空格打印一个句子
【发布时间】:2026-01-26 13:05:04
【问题描述】:

我看到了这个面试问题并试了一下。我被困。面试题是:

给定一个字符串

var s = "ilikealibaba";

还有一本字典

var d = ["i", "like", "ali", "liba", "baba", "alibaba"];

尽量给 s 留出最小的空间

输出可能是

  1. 我喜欢阿里巴巴(2位)
  2. 我喜欢阿里巴巴(3格)

但是选1号

我有一些代码,但在打印时卡住了。 如果您有更好的方法来回答这个问题,请告诉我。

function isStartSub(part, s) {
  var condi = s.startsWith(part);
  return condi;
}

function getRestStr(part, s) {
  var len = part.length;
  var len1 = s.length;
  var out = s.substring(len, len1);
  return out;
}

function recPrint(arr) {
    if(arr.length == 0) {
        return '';
    } else {
        var str = arr.pop();
        return str + recPrint(arr);
    }

}

// NOTE: have trouble to print
// Or if you have better ways to do this interview question, please let me know
function myPrint(arr) {
    return recPrint(arr);
}

function getMinArr(arr) {
    var min = Number.MAX_SAFE_INTEGER;
    var index = 0;
    for(var i=0; i<arr.length; i++) {
        var sub = arr[i];
        if(sub.length < min) {
            min = sub.length;
            index = i;
        } else {

        }   
    }

    return arr[index];  
}

function rec(s, d, buf) {
    // Base
    if(s.length == 0) {
        return;
    } else {
    
    } 

    for(var i=0; i<d.length; i++) {
        var subBuf = [];

        // baba
        var part = d[i];
        var condi = isStartSub(part, s);

        if(condi) {
            // rest string  
      var restStr = getRestStr(part, s);
      rec(restStr, d, subBuf);
            subBuf.unshift(part);
            buf.unshift(subBuf);
        } else {

        }       
    } // end loop

}

function myfunc(s, d) {
    var buf = [];
    rec(s, d, buf);

    console.log('-- test --');
    console.dir(buf, {depth:null});

    return myPrint(buf);    
}


// Output will be
// 1. i like alibaba (with 2 spaces)
// 2. i like ali baba (with 3 spaces)
// we pick no.1, as it needs less spaces
var s = "ilikealibaba";
var d = ["i", "like", "ali", "liba", "baba", "alibaba"];
var out = myfunc(s, d);
console.log(out);

基本上,我的输出是,不知道如何打印它....

[ [ 'i', [ 'like', [ 'alibaba' ], [ 'ali', [ 'baba' ] ] ] ] ]

【问题讨论】:

  • d 不是字典,它是一个数组。
  • @MihaiAlexandru-Ionut 我很确定他们的意思是“字典”是一个非技术术语(“单词集合”)。
  • 这是动态编程中的经典练习 - 我认为它是 Cormen 的“算法简介”或“算法设计手册”的一部分,但我记得在其中之一中看到过。
  • 你可以使用 Trie 数据结构

标签: javascript algorithm dictionary


【解决方案1】:

这个问题最适合动态规划方法。子问题是“创建s 前缀的最佳方法是什么”。然后,对于给定的前缀s,我们会考虑所有匹配前缀末尾的单词,并使用较早前缀的结果选择最佳单词。

这是一个实现:

var s = "ilikealibaba";
var arr = ["i", "like", "ali", "liba", "baba", "alibaba"];

var dp = []; // dp[i] is the optimal solution for s.substring(0, i)
dp.push("");

for (var i = 1; i <= s.length; i++) {
    var best = null; // the best way so far for s.substring(0, i)

    for (var j = 0; j < arr.length; j++) {
        var word = arr[j];
        // consider all words that appear at the end of the prefix
        if (!s.substring(0, i).endsWith(word))
            continue;

        if (word.length == i) {
            best = word; // using single word is optimal
            break;
        }

        var prev = dp[i - word.length];
        if (prev === null)
            continue; // s.substring(i - word.length) can't be made at all

        if (best === null || prev.length + word.length + 1 < best.length)
            best = prev + " " + word;
    }
    dp.push(best);
}

console.log(dp[s.length]);

【讨论】:

  • 此问题未标记 Java。请把它翻译成javascript。
  • 这是目前唯一的 4 个答案,它实际上会产生有保证的最佳解决方案。
  • 如果字典有 1,000,000 个条目而字符串有 1000 个字符会怎样?
【解决方案2】:

pkpnd 的答案是正确的。但是单词字典往往是相当大的集合,并且在字符串的每个字符处迭代整个字典将是低效的。 (另外,为每个 dp 单元保存整个序列可能会消耗大量空间。)相反,我们可以在迭代字符串时将问题框定为:给定字符串的所有先前索引,这些索引具有字典匹配扩展返回(到开始或另一个匹配),当我们包含当前字符时,哪一个既是字典匹配,又具有较小的总长度。一般:

f(i) = min(
  f(j) + length(i - j) + (1 if j is after the start of the string)
)
for all j < i, where string[j] ended a dictionary match
  and string[j+1..i] is in the dictionary 

由于我们只在有匹配项时添加另一个j,而新匹配项只能延伸回之前的匹配项或字符串的开头,所以我们的数据结构可以是一个元组数组(best index this match extends back to, total length up to here)。如果当前字符可以将字典匹配扩展回我们已经拥有的另一条记录,我们将添加另一个元组。一旦匹配的子字符串大于字典中最长的单词,我们还可以通过提前退出向后搜索进行优化,并在我们向后迭代时构建子字符串以与字典进行比较。

JavaScript 代码:

function f(str, dict){
  let m = [[-1, -1, -1]];
  
  for (let i=0; i<str.length; i++){
    let best = [null, null, Infinity];
    let substr = '';
    let _i = i;
    
    for (let j=m.length-1; j>=0; j--){
      let [idx, _j, _total] = m[j];
      substr = str.substr(idx + 1, _i - idx) + substr;
      _i = idx;

      if (dict.has(substr)){
        let total = _total + 1 + i - idx;
      
        if (total < best[2])
          best = [i, j, total];
      }
    }

    if (best[0] !== null)
      m.push(best);
  }
  
  return m;
}


var s = "ilikealibaba";

var d = new Set(["i", "like", "ali", "liba", "baba", "alibaba"]);

console.log(JSON.stringify(f(s,d)));

我们可以追溯我们的结果:

[[-1,-1,-1],[0,0,1],[4,1,6],[7,2,10],[11,2,14]]

[11, 2, 14] means a total length of 14,
where the previous index in m is 2 and the right index
of the substr is 11
=> follow it back to m[2] = [4, 1, 6]
this substr ended at index 4 (which means the
first was "alibaba"), and followed m[1]
=> [0, 0, 1], means this substr ended at index 1
so the previous one was "like"

你有它:“我喜欢阿里巴巴”

【讨论】:

  • 这在某些情况下确实应该更快,原因有两个:(1)当字典单词大多很长时,内部循环迭代会更少; (2)当字典很大时(可能更可能),因为哈希表查找比枚举每个字典单词和测试要快得多。顺便说一句,您可以通过逐步通过建立在所有反向字典单词集上的 trie 替换散列查找来删除另一个 O(n) 因素 - 然后每个查找都是真正的 O(1) 而不是 O(n) + amortized O( 1).
  • @j_random_hacker 不确定我是否遵循您建议的加速。你能举个例子吗?据我了解,在我当前的解决方案中,字典集中子字符串的查找是 O(1),但每个 substr 的构造是任意的,事先未知,尽管我在向后迭代时优化了它的构造。跨度>
  • 抱歉,每次查找声称 O(1) 时间是错误的。我应该写的是,如果您在反转的字典字符串上构建一个 trie,那么在每个起始位置 i,您可以在文本位置 i、i-1、... 处遍历匹配字符的 trie,只检查那些j 其中 trie 表明 str[j .. i] 是一个完全反转的字典单词,并且当 trie 中不存在路径时停止。但我认为这不会买太多。
  • @j_random_hacker 如果预期查询的数量很大,也许从字典中构建一个 trie 是有意义的。一般来说,我们的实际任务可以概括为哪些相关的字典单词包含其他字典单词的序列。但是字典越大,问题就越复杂。
【解决方案3】:

当你被要求找到一个最短的答案时,Breadth-First Search 可能是一个可能的解决方案。或者你可以查看A* Search

这里是 A* 的工作示例(因为它比 BFS 要做的更少:)),基本上只是从 Wikipedia 文章中复制而来。所有“将字符串变成图形”的魔术都发生在 getNeighbors 函数中

https://jsfiddle.net/yLeps4v5/4/

var str = 'ilikealibaba'
var dictionary = ['i', 'like', 'ali', 'baba', 'alibaba']

var START = -1
var FINISH = str.length - 1

// Returns all the positions in the string that we can "jump" to from position i
function getNeighbors(i) {
    const matchingWords = dictionary.filter(word => str.slice(i + 1, i + 1 + word.length) == word)
    return matchingWords.map(word => i + word.length)
}

function aStar(start, goal) {
    // The set of nodes already evaluated
    const closedSet = {};

    // The set of currently discovered nodes that are not evaluated yet.
    // Initially, only the start node is known.
    const openSet = [start];

    // For each node, which node it can most efficiently be reached from.
    // If a node can be reached from many nodes, cameFrom will eventually contain the
    // most efficient previous step.
    var cameFrom = {};

    // For each node, the cost of getting from the start node to that node.
    const gScore = dictionary.reduce((acc, word) => { acc[word] = Infinity; return acc }, {})

    // The cost of going from start to start is zero.
    gScore[start] = 0

    while (openSet.length > 0) {
        var current = openSet.shift()
        if (current == goal) {
            return reconstruct_path(cameFrom, current)
        }

        closedSet[current] = true;

        getNeighbors(current).forEach(neighbor => {
            if (closedSet[neighbor]) {
                return      // Ignore the neighbor which is already evaluated.
            }

            if (openSet.indexOf(neighbor) == -1) {  // Discover a new node
                openSet.push(neighbor)
            }

            // The distance from start to a neighbor
            var tentative_gScore = gScore[current] + 1
            if (tentative_gScore >= gScore[neighbor]) {
                return      // This is not a better path.
            }

            // This path is the best until now. Record it!
            cameFrom[neighbor] = current
            gScore[neighbor] = tentative_gScore
        })
    }

    throw new Error('path not found')
}

function reconstruct_path(cameFrom, current) {
    var answer = [];
    while (cameFrom[current] || cameFrom[current] == 0) {
        answer.push(str.slice(cameFrom[current] + 1, current + 1))
        current = cameFrom[current];
    }
    return answer.reverse()
}

console.log(aStar(START, FINISH));

【讨论】:

  • 问题中没有图表,因此 BFS 和 A* 是无意义的方法(因为它们对图表进行操作)。
  • @pkpnd, ilikealibaba 是图表!
  • @BassemAkl 请指定节点和边,以便 BFS 是正确的方法。
  • 用更具体的代码示例更新了答案,展示了如何在这样的任务上使用图形算法。
  • @pkpnd 请查看更新的答案(getNeighbors 函数可以满足您的要求)
【解决方案4】:

您可以通过检查起始字符串并呈现结果来收集字符串的所有可能组合。

如果多个结果具有最小长度,则采用所有结果。

它可能不适用于仅包含相同基本字符串的字符串的极值,例如 'abcabc''abc'。在这种情况下,我建议使用最短的字符串并通过迭代来更新任何部分结果以查找更长的字符串并尽可能替换。

function getWords(string, array = []) {
    words
        .filter(w => string.startsWith(w))
        .forEach(s => {
            var rest = string.slice(s.length),
                temp = array.concat(s);
            if (rest) {
                getWords(rest, temp);
            } else {
                result.push(temp);
            }
        });
}


var string = "ilikealibaba",
    words = ["i", "like", "ali", "liba", "baba", "alibaba"],
    result = [];
    
getWords(string);

console.log('all possible combinations:', result);

console.log('result:', result.reduce((r, a) => {
    if (!r || r[0].length > a.length) {
        return [a];
    }
    if (r[0].length === a.length) {
        r.push(a);
    }
    return r;
}, undefined))

【讨论】:

  • 这种方法在最坏的情况下是指数时间(result 中有指数数量的元素),而 O(N^2) 解决方案存在。
  • 实际上如果没有找到单词它就会停止,因为过滤。
  • 我想你不明白我的意思。如果string = "aaaaaaaaaaaaaaaaaaa", words = ["a", "aa"] 怎么办?该示例足够小,可以放入评论中,但运行需要几秒钟。
【解决方案5】:

使用trie数据结构

  1. 根据字典数据构造trie数据结构
  2. 在句子中搜索所有可能的切片并构建解决方案树
  3. 深度遍历解树并对最终组合进行排序

const sentence = 'ilikealibaba';
const words = ['i', 'like', 'ali', 'liba', 'baba', 'alibaba',];

class TrieNode {
    constructor() { }
    set(a) {
        this[a] = this[a] || new TrieNode();
        return this[a];
    }
    search(word, marks, depth = 1) {
        word = Array.isArray(word) ? word : word.split('');
        const a = word.shift();
        if (this[a]) {
            if (this[a]._) {
                marks.push(depth);
            }
            this[a].search(word, marks, depth + 1);
        } else {
            return 0;
        }
    }
}

TrieNode.createTree = words => {
    const root = new TrieNode();
    words.forEach(word => {
        let currentNode = root;
        for (let i = 0; i < word.length; i++) {
            currentNode = currentNode.set(word[i]);
        }
        currentNode.set('_');
    });
    return root;
};

const t = TrieNode.createTree(words);

function searchSentence(sentence) {
    const marks = [];
    t.search(sentence, marks);
    const ret = {};
    marks.map(mark => {
        ret[mark] = searchSentence(sentence.slice(mark));
    });
    return ret;
}

const solutionTree = searchSentence(sentence);

function deepTraverse(tree, sentence, targetLen = sentence.length) {
    const stack = [];
    const sum = () => stack.reduce((acc, mark) => acc + mark, 0);
    const ret = [];
    (function traverse(tree) {
        const keys = Object.keys(tree);
        keys.forEach(key => {
            stack.push(+key);
            if (sum() === targetLen) {
                const result = [];
                let tempStr = sentence;
                stack.forEach(mark => {
                    result.push(tempStr.slice(0, mark));
                    tempStr = tempStr.slice(mark);
                });
                ret.push(result);
            }
            if(tree[key]) {
                traverse(tree[key]);
            }
            stack.pop();
        });
    })(tree);
    return ret;
}

const solutions = deepTraverse(solutionTree, sentence);

solutions.sort((s1, s2) => s1.length - s2.length).forEach((s, i) => {
    console.log(`${i + 1}. ${s.join(' ')} (${s.length - 1} spaces)`);
});
console.log('pick no.1');

【讨论】: