前言
在很多算法題中,要求查找一個(gè)特定的內(nèi)容,可能是一個(gè)數(shù)字,也有可能是一個(gè)子串,或者是題目定義的一個(gè)滿足某個(gè)性質(zhì)的元素...這種題如果使用暴力搜索或者嵌套循環(huán),往往會(huì)超時(shí),所以我們需要一種更好的方法來降低時(shí)間復(fù)雜度:雙索引技術(shù)。
起源之二分法
這種查找方法的老祖宗,就是我們高中就學(xué)習(xí)過的二分法了。當(dāng)然,在大學(xué)的數(shù)據(jù)結(jié)構(gòu)和算法的課程里,它的地位也非常重要,只因?yàn)樗且粋€(gè)O(logn)的算法。
那它的雙索引是什么呢?
開始索引和結(jié)束索引。即查找范圍的始索引和終索引。
上代碼:
function binarySearch(arr, target) {
var len = arr.length;
// 在arr[l...r]之中查找target
var l = 0,
r = len - 1;
while (l <= r) {
//防止大數(shù)溢出,所以mid不是~~(l+r)/2
var mid = l + ~~((r - l) / 2);
if (arr[mid] == target)
return mid;
if (arr[mid] > target)
r = mid - 1;
else
l = mid + 1;
}
return -1;
}
// 遞歸方式實(shí)現(xiàn)二分查找法
function __binarySearch2(arr, l, r, target){
if( l > r )
return -1;
var mid = l + ~~((r - l) / 2);
if( arr[mid] === target )
return mid;
else if( arr[mid] > target )
return __binarySearch2(arr, l, mid-1, target);
else
return __binarySearch2(arr, mid+1, r, target);
}
function binarySearch2(arr,target){
var len = arr.length;
return __binarySearch2( arr , 0 , len-1, target);
}
普通雙索引
以leetcode第283題為例:題目要求我們把非零元素全部排列在前面,0全部放到最后。
這時(shí)我們需要兩個(gè)索引:一個(gè)用來遍歷數(shù)組,一個(gè)用來計(jì)算非零元素的個(gè)數(shù)。
我們?cè)O(shè)第一個(gè)指針為i,第二個(gè)指針為k,數(shù)組遍歷[0...i]區(qū)間,最后得到的[0..k]區(qū)間里面全是非零元素,[k+1...i]是0
var moveZeroes = function (nums) {
let k = 0;
for (let i = 0; i < nums.length; i++) {
if(nums[i]){
nums[k++] = nums[i];
}
}
for(let i = k;i<nums.length;i++){
nums[i] = 0;
}
return nums;
};
優(yōu)化:交換非零元素和零元素即可
var moveZeroes = function (nums) {
let k = 0;
for (let i = 0; i < nums.length; i++) {
if (nums[i]) {
//防止極端情況:全是非零
if(i !== k)
[nums[k], nums[i]] = [nums[i], nums[k]];
k++;
}
}
return nums;
};
對(duì)撞指針
這是雙索引的一種特殊情況:即第一個(gè)索引在前,第二個(gè)索引在后,在程序運(yùn)行的過程中兩者不斷靠近,最后達(dá)到一個(gè)臨界范圍,程序結(jié)束。
以leetcode的167題為例:題目要求我們找出一個(gè)數(shù)組中相加為target的兩個(gè)數(shù),并返回他們的次序。
因?yàn)檩斎氲臄?shù)組是已經(jīng)排序好的,所以雙索引的最初指向分別為首和尾。
我們假設(shè)兩個(gè)指針為i和j,[i...j]范圍內(nèi)就是我們關(guān)注的,如果numbers[i]+numbers[j]等于target返回結(jié)果,小了就讓i右移,大了就讓j左移
var twoSum = function (numbers, target) {
let i = 0, j = numbers.length - 1;
while (i < j) {
if (numbers[i] + numbers[j] === target) return [i + 1, j + 1]
else if (numbers[i] + numbers[j] < target) i++
else j--
}
return []
};
滑動(dòng)窗口
另一種雙索引的特殊情況就是我們需要讓兩個(gè)索引保持一定的相對(duì)距離,這種做法非常類似一個(gè)滑動(dòng)的窗口,在這個(gè)窗口中找到了答案就結(jié)束程序,沒有找到的話就滑動(dòng)之。
以leetcode的209題為例:給一個(gè)數(shù)組和一個(gè)target,找出數(shù)組中能夠相加得到target的最少元素的子數(shù)組。
這個(gè)題用暴力遍歷時(shí)間復(fù)雜度就是O(n3),就算優(yōu)化到O(n2)還是有點(diǎn)讓人接受不了,但是使用滑動(dòng)窗口解題的話,時(shí)間復(fù)雜度就變成了O(n)
我們假設(shè)兩個(gè)索引l和r,分別代表窗口的左索引和右索引,讓這個(gè)窗口的數(shù)字逐個(gè)相加看是否能得到答案,并且記錄長(zhǎng)度,因?yàn)槲覀兇鸢敢氖窃刈钌俚淖訑?shù)組。
var minSubArrayLen = function (s, nums) {
let l = 0;
let r = -1;//nums[l,r]為滑動(dòng)窗口
let res = nums.length + 1;
let sum = 0;
while (l < nums.length) {
if (r + 1 < nums.length && sum < s) {
r++;
sum += nums[r];
}
else {
sum -= nums[l++];
}
if (sum >= s) res = Math.min(res, r - l + 1);
}
//res沒有更新,沒有結(jié)果
if (res == nums.length + 1) return 0;
return res;
};
在滑動(dòng)窗口中做記錄
這是滑動(dòng)窗口的加強(qiáng)版,就像上一道題需要記錄窗口長(zhǎng)度一樣,有時(shí)候我們還需要記錄其他的信息來幫助解題
以leetcode第3題為例:題目要求我們找出最長(zhǎng)的無重復(fù)字符的子串,我們除了需要一個(gè)滑動(dòng)窗口外還需要一個(gè)數(shù)據(jù)結(jié)構(gòu)用來存儲(chǔ)字符的出現(xiàn)次數(shù)(數(shù)組和哈希表都可以)
我們的滑動(dòng)窗口每次檢測(cè)到重復(fù)字符就移動(dòng),并且記錄窗口長(zhǎng)度,最后得出答案。
var lengthOfLongestSubstring = function (s) {
let l = 0;
let r = -1;//s[l,r]是滑動(dòng)窗口
let rst = 0;
//其實(shí)freq的狀態(tài)就是0和1
let freq = new Array(256).fill(0);
while (l < s.length) {
if (r + 1 < s.length && freq[s[r + 1].charCodeAt()] === 0) {
r++;
freq[s[r].charCodeAt()]++;
}
//l和r所指的字母都一樣
else {
freq[s[l].charCodeAt()]--;
l++;
}
rst = Math.max(rst, r - l + 1);
}
return rst;
};