详解 diff 算法中的 list-diff 字符串的最小编辑距离问题

内容

list-diff 源码[1] 代码是大佬 livoras[2] 所写,本文对此代码进行详细解释

列表对比算法

问题

  • 如果列表中元素的顺序为 1 2 3 4,现在要把列表中元素的顺序移动为 2 1 3 4,应该怎样做呢?
  • 相信你肯定想到了,应该把 12 交换位置;咱们现在把交换位置这个操作,可以看成删除第0个位置的元素,和在第1个位置添加元素1的结合。
  • 所以列表中元素的操作就只有增加和删除。

贪心算法

  • 问题:如果列表中元素的顺序为 1 2 3 4,现在要把列表中元素的顺序移动为 4 2 3 1,应该怎样做呢?

  • 这时,我们可以利用贪心算法的思路来进行思考【也就是要尽可能少的进行移动,让新列表和旧列表同一位置的元素相同。根据这个想法,如果想让新列表和旧列表同一位置的元素相同,可以删除旧列表这个位置的元素,再添加正确的元素。上面这个过程需要两步,也就是说,一个位置的元素操作最多两步,使用贪心算法,操作必须为零步一步

  • 零步:那就是新旧列表中同一位置元素相同,不需要进行任何操作。

  • 一步:操作可以为增加或删除。这两种操作应该如何选择呢?

    再次回到贪心算法的目的,让新旧列表中同一位置的元素相同,如果在这个位置增加和新列表相同的元素,当然可以使得元素相同。那么什么时候选择删除呢?如果要选择删除,说明了,删除之后,相同位置的元素变得相同。

    例如: 1 2 3 4 要变成 2 1 3 4 首先遍历到第一个元素,发现不相同,可以增加2;检查1后面的那个元素2,发现和当前的元素相同,选择删除 删除之后变为 2 3 4 继续遍历下一个元素 3,发现和 1 不相同,可以增加1;检查 3 后面的元素是 4,发现和当前元素不相同,所以增加 1 增加之后变为 2 1 3 4 继续往后遍历,发现都相同

  • 所以目前咱们的遍历算法就设计好了

注意

  • 上面算法可以提高效率的前提是新旧列表中至少有一部分是相同的,只是位置不同

  • 比如 1 2 3 43 4 2 13 4 这部分是相同的,所以可以利用上述算法来提高效率

  • 如果是 1 2 3 44 3 2 1 完全没有相同的部分,只能在每一个位置增加新元素,然后删掉多余的部分“` // 例如: 1 2 3 4 4 3 2 1 1 2 3 4 4 3 2 1 // 删除后面的




key 的作用

key 存在(新旧列表全部都设置 key)

  • 如果有 key 的话,可以很容易定位旧列表的元素在新列表中存不存在,不需要每次都遍历新列表
  • 如果旧列表中的元素在新列表中不存在,那么就将旧列表中的这个元素删除
  • 至此,旧列表中的元素都存在于新列表中,但是新列表中的元素不一定都在旧列表中
  • 问题变成了使用上面的列表对比算法来使新旧列表元素相同。

key 不存在(新旧列表全部都没有设置 key)

  • 因为不知道新旧列表中是否有某些部分是相同的,所以不能使用上述算法提高效率。直接返回 {}。

新旧列表中一部分设置了 key,另一部分没有设置 key

  • 对设置了 key 的使用算法。
  • 没有设置 key 的,直接在旧列表这个位置增加这个元素

代码

删除旧列表多余的元素

根据元素 key 来判断是否需要删除
  • 遍历旧集合,根据 key 判断这个元素是否在新集合中存在。如果不存在,将 children 加入null,说明将来是要删除的。如果存在,将这个元素直接加入 children
删除在旧列表中属性不为key且这个位置不需要的元素
  • 如果新列表中没有添加 key 的元素个数要小于旧列表中没有添加 key 的元素个数,那么要将旧列表中一部分位置删除,直到和新列表中没有添加 key 的元素个数相同。
代码
// oldList = [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}]
// newList = [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}]
// key = 'id'
function diff (oldList, newList, key) {
  var oldMap = makeKeyIndexAndFree(oldList, key)
  var newMap = makeKeyIndexAndFree(newList, key)

  var newFree = newMap.free

  var oldKeyIndex = oldMap.keyIndex
  var newKeyIndex = newMap.keyIndex

  var moves = []

  var children = []
  var i = 0
  var item
  var itemKey
  var freeIndex = 0

  // oldList = [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}]
  while (i < oldList.length) {
    // item {id: "a"}
    item = oldList[i]
    // itemKey a
    itemKey = getItemKey(item, key)  
    
    if (itemKey) {
      // newList = [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}]
      // newKeyIndex: [c:0, a:1, b:2, e: 3, f: 4]
      if (!newKeyIndex.hasOwnProperty(itemKey)) {
        children.push(null)
      } else {
        var newItemIndex = newKeyIndex[itemKey]
        children.push(newList[newItemIndex])
      }
    } else {
      // 删除在旧列表中属性不为key且这个位置不需要的元素
      var freeItem = newFree[freeIndex++]
      children.push(freeItem || null)
    }
    i++
  }

  // 执行删除操作
  // oldList = [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}]
  // newList = [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}]
  // children [{id:"a"},{id:"b"},{id:"c"},null,{id:"e"}]
  var simulateList = children.slice(0)

  i = 0
  while (i < simulateList.length) {
    if (simulateList[i] === null) {
      remove(i)               // 加入操作数组
      removeSimulate(i)       // 从 simulateList 中移除
    } else {
      i++
    }
  }

  // 操作函数
  function remove (index) {
    var move = {index: index, type: 0}
    moves.push(move)
  }

  function insert (index, item) {
    var move = {index: index, item: item, type: 1}
    moves.push(move)
  }

  function removeSimulate (index) {
    simulateList.splice(index, 1)
  }
}

/**
 * Convert list to key-item keyIndex object.
 * 将列表转换为 key-item 的键值对象
 * [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}] -> [a:0,b:1,c:2...]
 * @param {Array} list
 * @param {String|Function} key
 */
function makeKeyIndexAndFree (list, key) {
  var keyIndex = {}
  var free = []
  for (var i = 0, len = list.length; i < len; i++) {
    var item = list[i]
    var itemKey = getItemKey(item, key)
    if (itemKey) {
      keyIndex[itemKey] = i
    } else {
      free.push(item)
    }
  }
  return {
    keyIndex: keyIndex,
    free: free
  }
}

// 获取置顶key的value
function getItemKey (item, key) {
  if (!item || !key) return void 666
  return typeof key === 'string'
    ? item[key]
    : key(item)       // 如果传来的 key 是一个函数,例如function(item){ return item }
}

使用列表对比算法对新旧列表进行操作

  // 遍历 newList 和 simulateList     
  // oldList = [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}]
  // newList = [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}]            i
  // simulateList [{id:"a"},{id:"b"},{id:"c"},{id:"e"}]                           j        
  var j = i = 0
  while (i < newList.length) {
    item = newList[i]
    itemKey = getItemKey(item, key)                       // c

    var simulateItem = simulateList[j]                    // {id:"a"}
    var simulateItemKey = getItemKey(simulateItem, key)   // a

    // newList 不为空或者 oldList 不为空
    if (simulateItem) {
      // 新旧集合中 key-value 相等,而且位置相同 ———————— 列表对比算法相同的那一种情况
      // 或者是  
      // undefined === undefined 说明新旧集合中都没有设置 key,不做任何操作
      if (itemKey === simulateItemKey) {
        j++
      } else {
        // 旧集合中没有设置 key
        // 列表对比算法是针对设置了 key 的算法,没有设置就直接插入
        if (!oldKeyIndex.hasOwnProperty(itemKey)) {
          insert(i, item)
        } else {
          // 列表对比算法不相同的那一种情况【列表对比算法是针对设置了 key 的算法】
          // 获取下一个位置的元素
          var nextItemKey = getItemKey(simulateList[j + 1], key)
          // 如果设置了 key 且和当前元素【itemKey】相同,删除这个位置的元素
          if (nextItemKey === itemKey) {
            remove(i)
            removeSimulate(j)
            j++ 
          } else {
            // 增加
            insert(i, item)
          }
        }
      }
    } else {
      // 这个语句可以执行的条件是:oldList为空,而newList不为空
      insert(i, item)   
    }
    i++
  }

  // 移除掉多余的元素
  var k = simulateList.length - j
  while (j++ < simulateList.length) {
    k--
    remove(k + i)
  }


完整代码


// oldList = [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}]
// newList = [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}]
function diff (oldList, newList, key) {
  // oldMap: [a:0,b:1,c:2...]
  var oldMap = makeKeyIndexAndFree(oldList, key)
  var newMap = makeKeyIndexAndFree(newList, key)

  var newFree = newMap.free

  var oldKeyIndex = oldMap.keyIndex
  var newKeyIndex = newMap.keyIndex

  var moves = []

  // a simulate list to manipulate
  var children = []
  var i = 0
  var item
  var itemKey
  var freeIndex = 0

  // oldList = [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}]
  while (i < oldList.length) {
    // item {id: "a"}
    item = oldList[i]
    // itemKey a
    itemKey = getItemKey(item, key)  
    
    if (itemKey) {
      // newList = [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}]
      // newKeyIndex: [c:0, a:1, b:2, e: 3, f: 4]
      if (!newKeyIndex.hasOwnProperty(itemKey)) {
        children.push(null)
      } else {
        var newItemIndex = newKeyIndex[itemKey]
        children.push(newList[newItemIndex])
      }
    } else {
      // 删除在旧列表中属性不为key且这个位置不需要的元素
      var freeItem = newFree[freeIndex++]
      children.push(freeItem || null)
    }
    i++
  }

  // 执行删除操作
  // oldList = [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}]
  // newList = [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}]
  // children [{id:"a"},{id:"b"},{id:"c"},null,{id:"e"}]
  var simulateList = children.slice(0)

  i = 0
  while (i < simulateList.length) {
    if (simulateList[i] === null) {
      remove(i)               // 加入操作数组
      removeSimulate(i)       // 从 simulateList 中移除
    } else {
      i++
    }
  }

 
  // 遍历 newList 和 simulateList     
  // oldList = [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}]
  // newList = [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}]            i
  // simulateList [{id:"a"},{id:"b"},{id:"c"},{id:"e"}]                           j        
  var j = i = 0
  while (i < newList.length) {
    item = newList[i]
    itemKey = getItemKey(item, key)                       // c

    var simulateItem = simulateList[j]                    // {id:"a"}
    var simulateItemKey = getItemKey(simulateItem, key)   // a

    // newList 不为空或者 oldList 不为空
    if (simulateItem) {
      // 新旧集合中 key-value 相等,而且位置相同 ———————— 列表对比算法相同的那一种情况
      // 或者是  
      // undefined === undefined 说明新旧集合中都没有设置 key,不做任何操作
      if (itemKey === simulateItemKey) {
        j++
      } else {
        // 旧集合中没有设置 key
        // 列表对比算法是针对设置了 key 的算法,没有设置就直接插入
        if (!oldKeyIndex.hasOwnProperty(itemKey)) {
          insert(i, item)
        } else {
          // 列表对比算法不相同的那一种情况【列表对比算法是针对设置了 key 的算法】
          // 获取下一个位置的元素
          var nextItemKey = getItemKey(simulateList[j + 1], key)
          // 如果设置了 key 且和当前元素【itemKey】相同,删除这个位置的元素
          if (nextItemKey === itemKey) {
            remove(i)
            removeSimulate(j)
            j++ 
          } else {
            // 增加
            insert(i, item)
          }
        }
      }
    } else {
      // 这个语句可以执行的条件是:oldList为空,而newList不为空
      insert(i, item)   
    }
    i++
  }

  // 移除掉多余的元素
  var k = simulateList.length - j
  while (j++ < simulateList.length) {
    k--
    remove(k + i)
  }


  function remove (index) {
    var move = {index: index, type: 0}
    moves.push(move)
  }

  function insert (index, item) {
    var move = {index: index, item: item, type: 1}
    moves.push(move)
  }

  function removeSimulate (index) {
    simulateList.splice(index, 1)
  }

  return {
    moves: moves,
    children: children
  }
}

/**
 * Convert list to key-item keyIndex object.
 * 将列表转换为 key-item 的键值对象
 * [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}] -> [a:0,b:1,c:2...]
 * @param {Array} list
 * @param {String|Function} key
 */
function makeKeyIndexAndFree (list, key) {
  var keyIndex = {}
  var free = []
  for (var i = 0, len = list.length; i < len; i++) {
    var item = list[i]
    var itemKey = getItemKey(item, key)
    if (itemKey) {
      keyIndex[itemKey] = i
    } else {
      free.push(item)
    }
  }
  return {
    keyIndex: keyIndex,
    free: free
  }
}

// 获取置顶key的value
function getItemKey (item, key) {
  if (!item || !key) return void 666
  return typeof key === 'string'
    ? item[key]
    : key(item)       // 如果传来的 key 是一个函数,例如function(item){ return item }
}

exports.makeKeyIndexAndFree = makeKeyIndexAndFree // exports for test
exports.diff = diff

本文转自 https://juejin.cn/post/7078934768645046286,如有侵权,请联系删除。

参考资料

[1]

https://github.com/livoras/list-diff/blob/master/lib/diff.js: https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Flivoras%2Flist-diff%2Fblob%2Fmaster%2Flib%2Fdiff.js

[2]

https://www.zhihu.com/people/livoras.com: https://link.juejin.cn/?target=https%3A%2F%2Fwww.zhihu.com%2Fpeople%2Flivoras.com


版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/20471.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!