本篇分析下sorted
家族的方法,该系列的方法主要是用来确定向已排序数组中插入 value 的索引。包括sortedIndex
、sortedLastIndex
、sortedIndexOf
、sortedLastIndexOf
、sortedIndexBy
、sortedLastIndexBy
、sortedUniq
、sortedUniqBy
以及核心方法baseSortedIndex
、baseSortedIndexBy
、baseSortedUniq
。
首先学习一个二分查找的实例,这也是sortedIndex
系列方法用到的核心算法。顺便分析依赖到的 lodash 方法eq
。
具体的依赖路径图如下所示:
二分查找 二分查找(Binary Search)是一种查找算法,用来在一个数组中查找指定的元素。注意这个数组需要是一个有序数组才有效,时间复杂度为O(log2n)
。下面是一个二分查找与线性查找的对照图 。
最普通的二分查找 下面给出最普通的二分查找的 JS 实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function binarySearch (array, value ) { let low = 0 ; let high = array.length - 1 ; while (low <= high) { let mid = Math .floor((low + high) / 2 ); if (value == array[mid]) { return mid; } else if (array[mid] < value) { low = mid + 1 ; } else if (value < array[mid]) { high = mid - 1 ; } } }
按照示例图给的数据进行计算:
1 2 const array = [1 , 3 , 5 , 7 , 11 , 13 , 17 , 19 , 23 , 29 , 31 , 37 , 41 , 43 , 47 , 53 , 59 ];binarySearch(array, 37 );
迭代
low
high
mid
array[mid]
1
0
16
8
23
2
9
16
12
41
3
9
11
10
31
4
11
11
11
37
但是这个算法对于某些情况是有一些问题的,当数组中要寻找的 value 存在有多个重复时,他只能随缘找到其中一个,无法找到最左边或者最右边那个。但是 sorted 算法中是需要这种能力的,所以需要进化!
二分算法寻找左侧边界 先放出寻找左侧边界代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function binarySearchLeft (array, value ) { let low = 0 ; let high = array.length; while (low < high) { let mid = Math .floor((low + high) / 2 ); if (value == array[mid]) { high = mid; } else if (array[mid] < value) { low = mid + 1 ; } else if (value < array[mid]) { high = mid; } } return low; }
与普通二分算法的区别是以下几点:
初始 high
给的是 array.length
,没有减 1
。
判断条件是 <
,而不是 <=
。
当 value == array[mid]
时,并不立即返回 mid
,而是继续向左查找。
当 value < array[mid]
时,high = mid
,而不是 high = mid - 1
。
1
和 4
是一样的原因,不需要减 1
就是因为 2
中判断条件变成了<
而不是<=
。第 3
条是寻找左侧边界的关键,如果发现了与value
相等的mid
值,不要立即返回,而是把这里设为high
,继续向左寻找有没有更小的,直到找到最左侧的位置。
有同学可能会问为啥不只改第3
条,从而继续用<=
的版本。这是因为如果value
比array
中的所有值都大时,low
会比最大索引还大,导致越界
。
二分算法寻找右侧边界 继续放出寻找右侧边界代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function binarySearchRight (array, value ) { let low = 0 ; let high = array.length; while (low < high) { let mid = Math .floor((low + high) / 2 ); if (value == array[mid]) { low = mid + 1 ; } else if (array[mid] < value) { low = mid + 1 ; } else if (value < array[mid]) { high = mid; } } return low; }
寻找右侧边界和左侧同理,只不过区别 3
变成了 low = mid + 1
。
依赖的内置方法 两个依赖的核心内置方法都是用到了二分查找算法的变形。
baseSortedIndex 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import baseSortedIndexBy from './baseSortedIndexBy.js' ;import isSymbol from '../isSymbol.js' ;const MAX_ARRAY_LENGTH = 4294967295 ;const HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1 ;function baseSortedIndex (array, value, retHighest ) { let low = 0 ; let high = array == null ? low : array.length; if ( typeof value === 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH ) { while (low < high) { const mid = (low + high) >>> 1 ; const computed = array[mid]; if ( computed !== null && !isSymbol(computed) && (retHighest ? computed <= value : computed < value) ) { low = mid + 1 ; } else { high = mid; } } return high; } return baseSortedIndexBy(array, value, (value) => value, retHighest); } export default baseSortedIndex;
baseSortedIndexBy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 import isSymbol from '../isSymbol.js' ;const MAX_ARRAY_LENGTH = 4294967295 ;const MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1 ;function baseSortedIndexBy (array, value, iteratee, retHighest ) { let low = 0 ; let high = array == null ? 0 : array.length; if (high == 0 ) { return 0 ; } value = iteratee(value); const valIsNaN = value !== value; const valIsNull = value === null ; const valIsSymbol = isSymbol(value); const valIsUndefined = value === undefined ; while (low < high) { let setLow; const mid = Math .floor((low + high) / 2 ); const computed = iteratee(array[mid]); const othIsDefined = computed !== undefined ; const othIsNull = computed === null ; const othIsReflexive = computed === computed; const othIsSymbol = isSymbol(computed); if (valIsNaN) { setLow = retHighest || othIsReflexive; } else if (valIsUndefined) { setLow = othIsReflexive && (retHighest || othIsDefined); } else if (valIsNull) { setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull); } else if (valIsSymbol) { setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol); } else if (othIsNull || othIsSymbol) { setLow = false ; } else { setLow = retHighest ? computed <= value : computed < value; } if (setLow) { low = mid + 1 ; } else { high = mid; } } return Math .min(high, MAX_ARRAY_INDEX); } export default baseSortedIndexBy;
baseSortedUniq 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import eq from '../eq.js' ;function baseSortedUniq (array, iteratee ) { let seen; let index = -1 ; let resIndex = 0 ; const { length } = array; const result = []; while (++index < length) { const value = array[index], computed = iteratee ? iteratee(value) : value; if (!index || !eq(computed, seen)) { seen = computed; result[resIndex++] = value === 0 ? 0 : value; } } return result; } export default baseSortedUniq;
依赖的 lodash 方法 eq 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 function eq (value, other ) { return value === other || (value !== value && other !== other); } export default eq;
sorted 家族 下面是六个sorted
系列方法的解析。
sortedIndex 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import baseSortedIndex from './.internal/baseSortedIndex.js' ;function sortedIndex (array, value ) { return baseSortedIndex(array, value); } export default sortedIndex;
sortedLastIndex 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import baseSortedIndex from './.internal/baseSortedIndex.js' ;function sortedLastIndex (array, value ) { return baseSortedIndex(array, value, true ); } export default sortedLastIndex;
sortedIndexOf 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import baseSortedIndex from './.internal/baseSortedIndex.js' ;import eq from './eq.js' ;function sortedIndexOf (array, value ) { const length = array == null ? 0 : array.length; if (length) { const index = baseSortedIndex(array, value); if (index < length && eq(array[index], value)) { return index; } } return -1 ; } export default sortedIndexOf;
sortedLastIndexOf 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import baseSortedIndex from './.internal/baseSortedIndex.js' ;import eq from './eq.js' ;function sortedLastIndexOf (array, value ) { const length = array == null ? 0 : array.length; if (length) { const index = baseSortedIndex(array, value, true ) - 1 ; if (eq(array[index], value)) { return index; } } return -1 ; } export default sortedLastIndexOf;
sortedIndexBy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import baseSortedIndexBy from './.internal/baseSortedIndexBy.js' ;function sortedIndexBy (array, value, iteratee ) { return baseSortedIndexBy(array, value, iteratee); } export default sortedIndexBy;
sortedLastIndexBy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import baseSortedIndexBy from './.internal/baseSortedIndexBy.js' ;function sortedLastIndexBy (array, value, iteratee ) { return baseSortedIndexBy(array, value, iteratee, true ); } export default sortedLastIndexBy;
sortedUniq 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import baseSortedUniq from './.internal/baseSortedUniq.js' ;function sortedUniq (array ) { return array != null && array.length ? baseSortedUniq(array) : []; } export default sortedUniq;
sortedUniqBy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import baseSortedUniq from './.internal/baseSortedUniq.js' ;function sortedUniqBy (array, iteratee ) { return array != null && array.length ? baseSortedUniq(array, iteratee) : []; } export default sortedUniqBy;