算法:最長上升子序列

零、參考資料

 

一、大體思路

規律(思路)是:(注:結果集中存儲的是都是下標,不是原數組的數據)
1. 遍歷原數組,比對當前值與結果集最後一位
2. 如果當前值大,則推入結果集(注意,是推入,即拓寬結果集)
3. 如果當前值小,則用二分法(這也是一個優化)去找結果集中比當前值大的項,然後(用當前值在原數組中的下標)替換掉結果集中的結果
4. 前驅節點追溯,替換掉錯誤的節點 - 這是對第 3 步的修正

二、最簡單(理想)的情況

let origin = [10, 11, 12, 13, 14, 15, 16, 0];

function getSequence(origin = []) {
  const result = [0]; // 默認以數組中第 0 個爲基準來做序列,注意[存放的是數組索引]
  const len = origin.length;
  let resultLastIndex; // 結果集中最後一位的數據,而結果集記錄的是數據在 origin 數組中的下標

  for (let i = 0; i < len; i++) {
    const current = origin[i];
    
    // if (arrI !== 0) { // 在vue newIndexToOldIndexMap 中,0代表需要創建新元素,無需進行位置移動操作,此處不需要,先行註釋掉
    resultLastIndex = result[result.length - 1];
    
    if (current > origin[resultLastIndex]) { // 對應規律(思路) 2
      result.push(i);
      
      continue;
    }
    // }
  }

  return result;
}

let idxArr = getSequence(origin);
let rst = idxArr.map(idx => origin[idx]);

console.log(rst);

 

三、貪心 + 二分處理的情況

在最理想的情況之上處理規律(思路) 3
let origin = [10, 11, 12, 13, 14, 15, 16, 0];

function getSequence(origin = []) {
  const result = [0]; // 默認以數組中第 0 個爲基準來做序列,注意[存放的是數組索引]
  const len = origin.length;
  let resultLastIndex; // 結果集中最後一位的數據,而結果集記錄的是數據在 origin 數組中的下標
  let start, end, middle;

  for (let i = 0; i < len; i++) {
    const current = origin[i];
    
    // if (arrI !== 0) { // 在vue newIndexToOldIndexMap 中,0代表需要創建新元素,無需進行位置移動操作,此處不需要,先行註釋掉
    resultLastIndex = result[result.length - 1];
    
    if (current > origin[resultLastIndex]) { // 對應規律(思路) 2
      result.push(i);
      
      continue;
    }

    // 通過二分查找,在結果集中找到僅大於當前值的(所有大於當前值的結果中的最小值),用當前值的索引將其替換掉
    // 二分運算,對應規律(思路) 3
    start = 0;
    end = result.length - 1;

    while (start < end) {
      middle = ((start + end) / 2) | 0; // 向下取整

      if (current > origin[result[middle]]) {
        start = middle + 1;
      } else {
        end = middle;
      }
    }

    // 進行替換
    if (current < origin[result[end]]) {
      result[end] = i;
    }
    // }
  }

  return result;
}


let idxArr = getSequence(origin);
let rst = idxArr.map(idx => origin[idx]);

console.log(rst);

 

四、前驅節點追溯處理的情況

貪心 + 二分處理的場景覆蓋了所有情況,但是這裏有個問題:如果源數組是 [102, 103, 101, 105, 106, 108, 107, 109, 104] ,那麼篩選出的子序列是 [101, 103, 104, 106, 107, 109] ,
而正確的應該是 [102, 103, 105, 106, 107, 109] ,即長度沒問題,內部數據確是錯的

所以,需要引入一個回溯列表 p,來記錄一些數據,其長度和 origin 數組長度一致,所有位置上均爲 0,這個數組記錄的也是數據在 origin 中的下標,如果當前數據進入到了結果集,則記錄下(結果集中)前值(具體看代碼和圖示)

 

let origin = [102, 103, 101, 105, 106, 108, 107, 109, 104];

function getSequence(origin = []) {
  const result = [0]; // 默認以數組中第 0 個爲基準來做序列,注意[存放的是數組索引]
  const len = origin.length;
  const p = new Array(len).fill(0);  // 最後要標記索引,放的東西不用關心,但是要和源數組一樣長
  // const p = Array.from({ length: len }, () => 0); // 另外一種寫法
  let resultLastIndex; // 結果集中最後一位的數據,而結果集記錄的是數據在 origin 數組中的下標
  let start, end, middle;

  for (let i = 0; i < len; i++) {
    const current = origin[i];
    
    // if (current !== 0) { // 在vue newIndexToOldIndexMap 中,0代表需要創建新元素,無需進行位置移動操作,此處不需要,先行註釋掉
    resultLastIndex = result[result.length - 1];
    
    if (current > origin[resultLastIndex]) { // 對應規律(思路) 2
      result.push(i);
      p[i] = resultLastIndex; // 當前放到末尾的要記錄他前面的索引,用於追溯

      continue;
    }

    // 通過二分查找,在結果集中找到僅大於當前值的(所有大於當前值的結果中的最小值),用當前值的索引將其替換掉
    start = 0;
    end = result.length - 1;

    while (start < end) {
      middle = ((start + end) / 2) | 0; // 向下取整

      if (current > origin[result[middle]]) {
        start = middle + 1;
      } else {
        end = middle;
      }
    }

    if (current < origin[result[end]]) {
      result[end] = i;

      p[i] = result[end - 1]; // 記住前值, 其實就是和 p[i] = resultLastIndex; 這句在不同的邏輯分支上
    }
    // }
  }

  // 對應規律(思路) 4,從 p 的末尾開始回溯修正
  let i = result.length;
  let last = result[i - 1];

  while(i-- > 0) {
    result[i] = last;
    last = p[last];
  }

  return result;
}

let result = getSequence(origin);

console.log(result.map(r => origin[r]))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章