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

内容

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

列表对比算法

问题
  • 如果列表中元素的顺序为 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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值