1、巧用陣列下標
例:在統計一個字串中字幕出現的次數時,我們就可以把這些字母當做陣列下標,在遍歷的時候,如果字母A遍歷到,則arr[`a`]就可以加1了。
法一:利用物件的key值不重複
var str = `hajshdjldjf`;
function count(str){
var obj = {};
for(var i = 0; i < str.length; i++){
if(obj[str[i]]){
obj[str[i]]++;
}else{
obj[str[i]] = 1;
}
}
console.log(obj);
return obj;
}
count(str);複製程式碼
法二:利用陣列的下標
var str = `hajshdjldjf`;
function count(str){
var arr = [];
for(var i = 0; i < str.length; i++){
if(arr[str[i]]){
arr[str[i]]++;
}else{
arr[str[i]] = 1;
}
}
}
count(str);複製程式碼
其實這兩種方法的思想是一致的。
例:給你n個無序的int整型陣列arr,並且這些整數的取值範圍都在0-20之間,要你在 O(n) 的時間複雜度中把這 n 個數按照從小到大的順序列印出來。
對於這道題,如果你是先把這 n 個數先排序,再列印,是不可能O(n)的時間列印出來的。但是數值範圍在 0-20。我們就可以巧用陣列下標了。把對應的數值作為陣列下標,如果這個數出現過,則對應的陣列加1。
利用物件:
var arr = [1,2,3,4,5,4,5,6,6,7,6,9,17,16,15,14,12,11];
//常規解法 利用物件的key值不能重複去計算次數
//res去記錄數字出現的順序
function fn(arr){
arr.sort(function(a,b){
return a - b;
});
var res = [];
var resdetail = [];
for(var i = 0; i < arr.length; i++){
if(res.length === 0 || res[res.length-1] !== arr[i]){
res.push(arr[i]);
var obj = {
key:arr[i],
value:1
};
resdetail.push(obj);
}else{
resdetail[resdetail.length-1].value++;
}
}
console.log(resdetail);
return resdetail;
}
fn(arr);複製程式碼
利用陣列下標
var arr = [1,2,3,4,5,4,5,6,6,7,6,9,17,16,15,14,12,11];
//利用陣列下標 時間複雜度為O(n)
function fn(arr){
var temp = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
for(var i = 0; i < arr.length; i++){
temp[arr[i]]++;
}
for(var j = 0; j < 21; j++){
for(var k = 0; k < temp[j]; k++){
console.log(j);
}
}
}
fn(arr);複製程式碼
2、巧用取餘
有時候我們在遍歷陣列的時候,會進行越界判斷,如果下標差不多要越界了,我們就把它置為0重新遍歷。特別是在一些環形的陣列中,例如用陣列實現的佇列。往往會寫出這樣的程式碼:
for (int i = 0; i < N; i++)
{
if (pos < N) {
//沒有越界
// 使用陣列arr[pos]
else
{
pos = 0;//置為0再使用陣列
//使用arr[pos]
}
pos++;
}
}複製程式碼
實際上我們可以通過取餘的方法來簡化程式碼
for (int i = 0; i < N; i++) {
//使用陣列arr[pos] (我們假設剛開始的時候pos < N)
pos = (pos + 1) % N;
}複製程式碼
3、巧用雙指標
對於雙指標,在做關於單連結串列的題是特別有用,比如“判斷單連結串列是否有環”、“如何一次遍歷就找到連結串列中間位置節點”、“單連結串列中倒數第 k 個節點”等問題。對於這種問題,我們就可以使用雙指標了,會方便很多。
(1)判斷單連結串列中是否有環
我們可以設定一個快指標和一個慢指標,慢的一次移動一個節點,快的一次移動兩個節點。如果存在環,快指標會在第二次遍歷時和慢指標相遇。
(2)如何一次遍歷就找到連結串列中間位置節點
一樣是設定一個快指標和慢指標。慢的一次移動一個節點,快的一次移動兩個。當快指標遍歷完成時,慢指標剛好到達中點。
(3)單連結串列中倒數第 k 個節點
設定兩個指標,其中一個指標先移動k-1步,從第k步開始,兩個指標以相同的速度移動。當那個先移動的指標遍歷完成時,第二個指標指向的位置即倒數第k個位置。
4、巧用位移
有時候我們在進行除數或乘數運算的時候,例如n / 2,n / 4, n / 8這些運算的時候,我們就可以用移位的方法來運算了,這樣會快很多。
例如:
n / 2 等價於 n >> 1
n / 4 等價於 n >> 2
n / 8 等價於 n >> 3。
這樣通過移位的運算在執行速度上是會比較快的,也可以顯的你很厲害的樣子,哈哈。
還有一些 &(與)、|(或)的運算,也可以加快運算的速度。例如判斷一個數是否是奇數,你可能會這樣做
if(n % 2 == 1){
dosomething();
}複製程式碼
不過我們用與或運算的話會快很多。例如判斷是否是奇數,我們就可以把n和1相與了,如果結果為1,則是奇數,否則就不會。即
if(n & 1 == 1){
dosomething();
)複製程式碼
5、設定哨兵位
在連結串列的相關問題中,我們經常會設定一個頭指標,而且這個頭指標是不存任何有效資料的,只是為了操作方便,這個頭指標我們就可以稱之為哨兵位了。
例如我們要刪除頭第一個節點是時候,如果沒有設定一個哨兵位,那麼在操作上,它會與刪除第二個節點的操作有所不同。但是我們設定了哨兵,那麼刪除第一個節點和刪除第二個節點那麼在操作上就一樣了,不用做額外的判斷。當然,插入節點的時候也一樣。
有時候我們在運算元組的時候,也是可以設定一個哨兵的,把arr[0]作為哨兵。例如,要判斷兩個相鄰的元素是否相等時,設定了哨兵就不怕越界等問題了,可以直接arr[i] == arr[i-1]?了。不用怕i = 0時出現越界。
6、與遞迴相關的一些優化
(1)對於可以遞迴的問題考慮狀態儲存。
當我們使用遞迴來解決一個問題時,很容易產生重複去算同一個子問題,這個時候我們要考慮狀態儲存以防止重複計算。
例:斐波那契數列
function fn(n){
if(n <= 2){
return 1;
}else{
return fn(n-1) + fn(n-2);
}
}
console.log(fn(10));複製程式碼
不過對於可以使用遞迴解決的問題,我們一定要考慮是否有很多重複計算。顯然對於 f(n) = f(n-1) + f(n-2) 的遞迴,是有很多重複計算的。如
就有很多重複計算了。這個時候我們要考慮狀態儲存。並且可以自底向上。
function fn(n){
var res = [];
res[0] = 1;
res[1] = 1;
for(var i = 2; i < n; i++){
res[i] = res[i-1] + res[i-2];
}
console.log(res[n-1]);
return res[n-1];
}
fn(10);複製程式碼
進一步優化:使用兩個變數。
function fn(n){
var a = 1;
var b = 1;
for(var i = 3; i <= n; i++){
a = a + b;
b = a - b;
}
return a;
}
fn(10);複製程式碼