フロントエンドアルゴリズムブラッシング問題の道~マージソート(8つの質問の詳細分析)

マージソート = 再帰 + マージ

コード

大きな配列を各項目に再帰的に分割し、2 つの項目の順序付けされたマージを実行します。マージ ソートはmerge、つまり 2 つのソートされた配列を 1 つのソートされた配列にマージすることに重点を置いています。2 つのポインター
を設定し、1 つは左側の配列の最初の項目を指し、もう 1 つは右側の配列の最初の項目を指し、2 つのポインターに対応する数値を比較し、小さい方を一時配列に入れてから移動します。対応する小さなポインタを右側に置き、両方のポインタが対応する配列の末尾を通過するまで継続的にループします。最後に、一時配列を元の配列の対応する位置に上書きするだけです。

var mergeSort = function (arr, left, right) {
    
    
    if (left >= right) return;

    let mid = Math.floor((left + right) / 2);
    //将左边数组变有序
    mergeSort(arr, left, mid)
    //将右边数组变有序
    mergeSort(arr, mid + 1, right)

    //将左右两个有序数组合并
    let p1 = left, p2 = mid + 1, saveArr = [];
    while (p1 <= mid || p2 <= right) {
    
    
        if ((p2 > right) || (p1 <= mid && arr[p1] < arr[p2])) {
    
    
            saveArr.push(arr[p1])
            p1++
        } else {
    
    
            saveArr.push(arr[p2])
            p2++
        }
    }

    //将合并后数组覆盖原数组
    for (let i = left; i <= right; i++) {
    
    
        arr[i] = saveArr[i - left]
    }
}

let arr = [5, 9, 1, 4, 3, 2, 8, 7, 6]
mergeSort(arr, 0, arr.length - 1)

console.log(arr); //[1, 2, 3, 4, 5, 6, 7, 8, 9]

マージソートの役割はソートだけにとどまらず、大きな問題をいくつかの小さな問題に分割し、小さな問題と小さな問題を横断する問題を解決することで大きな問題を解決するという考え方が重要です実践的な問題を通じてその威力を理解しましょう。

トピック

1. 逆順ペア問題

逆ペア問題

一連の考え

この質問の焦点は、マージ プロセス中の逆ペアの数を記録することです。
まず配列を降順に並べ替えます。2 つの配列をマージするプロセスでは、左側の配列の値が一時配列に入れられるたびに、両側の配列が降順になっているため、現在の値はすべての値より小さくなります。右の配列ポインタから配列の末尾まで、どちらも大きいです。たった今右边数组末尾下标 - 右指针下标 = 逆序对个数

コード

/**
 * @param {number[]} nums
 * @return {number}
 */

var mergeSort = function (arr, l, r) {
    
    
    if (l >= r) return 0;

    let mid = Math.floor((l + r) / 2),
        //记录左右数组的逆序对个数
        Lnum = mergeSort(arr, l, mid),
        Rnum = mergeSort(arr, mid + 1, r),
        num = Lnum + Rnum;

    let left = l, right = mid + 1, sortArr = [];
    while (left <= mid || right <= r) {
    
    
        if ((right > r) || (left <= mid && arr[left] > arr[right])) {
    
    
            sortArr.push(arr[left]);
            // 左边的值加入临时数组,记录逆序对数量
            num += r - right + 1
            left++
        } else {
    
    
            sortArr.push(arr[right]);
            right++
        }
    }

    for (let i = l; i <= r; i++) {
    
    
        arr[i] = sortArr[i - l]
    }

    return num
}

var reversePairs = function (nums) {
    
    
    return mergeSort(nums, 0, nums.length - 1)
};

2. リンクされたリストを並べ替える

ソートされたリンクリスト

一連の考え

この問題は通常の再帰的ソートですが、配列からリンク リストへのソートです。
個人的には、分割はもう少し難しいと感じていて、最初はリンクリストを 1 回走査して長さを取得し、その半分の位置にポインタを移動させるので、2 回走査する必要があります。後で、他の人の速度ポインターを見て、コピーしました:)
マージプロセスについては何も言うことはありません。つまり、ポインターが左右にジャンプします。

コード

/**
* function ListNode(val, next) {
*    this.val = (val === undefined ? 0 : val)
*    this.next = (next === undefined ? null : next)
* }
**/

var merge = function (leftList, rightList) {
    
    
    let temp = new ListNode(), p = temp;
    
    // 将两个有序链表合并
    while (leftList && rightList) {
    
    
        if (leftList.val < rightList.val) {
    
    
            p.next = leftList
            leftList = leftList.next
        } else {
    
    
            p.next = rightList
            rightList = rightList.next
        }
        p = p.next
    }
    
    // 将剩下的一条链表加到结果链表的末尾
    if (leftList) {
    
    
        p.next = leftList
    } else if (rightList) {
    
    
        p.next = rightList
    }
    
    //返回合并后链表
    return temp.next
}

/**
* @param {ListNode} head
* @return {ListNode}
*/
var sortList = function (head) {
    
    
    if (!head || !head.next) return head;

    let fast = head.next, slow = head;
    // 快慢指针,快指针走到末尾,慢指针走到一半
    while (fast) {
    
    
        slow = slow.next
        fast = fast.next
        if (fast) fast = fast.next;
    }

    // 拆成两条有序链表
    let rightList = slow.next;
    slow.next = null
    let leftList = head
    let l = sortList(leftList)
    let r = sortList(rightList)

    return merge(l, r)
};

3. 2 つの二分探索ツリー内のすべての要素を配置します。

2 つの二分探索ツリー内のすべての要素

一連の考え

二分探索木の特性により、次のようになります左节点比根节点小,右节点比根节点大
したがって、バイナリ ツリーを順番に走査すると、順序付けされた配列が取得され、その 2 つの順序付けされた配列がマージされます。

コード

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */

// 中序遍历
var inorder = function (root) {
    
    
    if (!root) return [];
    let leftArr = inorder(root.left),
        rightArr = inorder(root.right);

    return [...leftArr, root.val, ...rightArr]
}

/**
 * @param {TreeNode} root1
 * @param {TreeNode} root2
 * @return {number[]}
 */
var getAllElements = function (root1, root2) {
    
    
    // 获得两个有序数组
    let leftList = inorder(root1),
        rightList = inorder(root2);

    // 合并两个有序数组
    let p1 = 0, p2 = 0, temp = [];
    while (p1 < leftList.length && p2 < rightList.length) {
    
    
        if (leftList[p1] < rightList[p2]) {
    
    
            temp.push(leftList[p1])
            p1++
        } else {
    
    
            temp.push(rightList[p2])
            p2++
        }
    }

    if (p1 < leftList.length) {
    
    
        temp.push(...leftList.slice(p1))
    } else if (p2 < rightList.length) {
    
    
        temp.push(...rightList.slice(p2))
    }

    return temp
};

4. 共通祖先

共通祖先

一連の考え

次の 3 つの状況があります。

  1. p、q のいずれかがルートです。
  2. p、q はそれぞれルートの左ノードと右ノードにあります。
  3. p、q はどちらもルートの同じ側にあります。

コード

/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */

var lowestCommonAncestor = function(root, p, q) {
    
    
    if(!root)return root;
    //第一种情况,返回当前节点
    if(root == p || root == q)return root;
    
    //寻找左右子树是否存在p,q
    let left = lowestCommonAncestor(root.left, p, q)
    let right = lowestCommonAncestor(root.right, p, q)
    
	//第二种情况,返回当前节点
    if(left && right)return root;
    
    //第三种情况,存在哪一侧就返回那一侧寻找到的结果
    return left ? left : right
};

5. 最も深いリーフノードの合計

最も深いリーフ ノードの合計

一連の考え

2 つの値を記録します。

  1. 見つかったノードの最大深さ
  2. 現在記録されている最大深度ノードの合計値

コード

var numSum = function (root, deep, arr) {
    
    
    if (!root) return;
    // 深度增加,覆盖旧值
    if (deep > arr[0]) {
    
    
        arr[1] = root.val
        arr[0] = deep
    } else if (deep == arr[0]) {
    
    
        // 深度相同,累加
        arr[1] += root.val
    }
    // 递归左右子节点
    numSum(root.left, deep + 1, arr)
    numSum(root.right, deep + 1, arr)
}

/**
 * @param {TreeNode} root
 * @return {number}
 */
var deepestLeavesSum = function (root) {
    
    
    // 第一项记录深度,第二项记录和值
    let arr = [0, 0]
    numSum(root, 0, arr)
    return arr[1]
};

6. 部分配列とソートされた区間の合計

部分配列とソートされた間隔の合計

一連の考え

まず配列全体を走査し、配列内のすべての部分配列の合計を計算し、次にマージ ソートを実行して順序付けされた配列を取得し、最後に
10^9 + 7 の法を取得して戻ります。

コード

var rangeSum = function (nums, n, left, right) {
    
    
    let list = []

    // 计算所有子数组和
    for (let i = 0; i < n; i++) {
    
    
        let temp = 0
        for (let j = i; j < n; j++) {
    
    
            temp += nums[j]
            list.push(temp)
        }
    }

    // 归并排序
    mergeSort(list, 0, list.length - 1)

    // 计算区间和
    let Sum = 0;
    for (let i = left-1; i <= right-1; i++) {
    
    
        Sum += list[i]
        Sum %= 1000000007
    }

    return Sum
};

7. 区間和の数

区間合計の数

一連の考え

間隔の合計と値の両方をプレフィックス合計の質問に変換できます。
プレフィックス合計の概念:
プレフィックスと
この機能を使用すると、元の配列の添字 i から添字 j までの合計は、プレフィックスの値と配列の添字 j から添字 i の値を引いた値になります。
たった今arr[i] + ... + arr[j] = prefix[j] - prefix[i]

コード

/**
 * @param {number[]} nums
 * @param {number} lower
 * @param {number} upper
 * @return {number}
 */

// 前缀和
var prefix = function(arr) {
    
    
    let temp = [0]
    for (let i = 0; i < arr.length; i++) {
    
    
        temp.push(temp[i]+arr[i])
    }
    return temp
}

var countRangeSum = function (nums, lower, upper) {
    
    
    let preList = prefix(nums);

    return mergeSort(preList,0,preList.length-1, lower, upper)
};

var mergeSort = function (arr, left, right, lower, upper) {
    
    
    if (left >= right) return 0;

    let mid = Math.floor((left + right) / 2);
    // 记录左右两边数组符合区间和的个数
    let leftNum = mergeSort(arr, left, mid, lower, upper)
    let rightNum = mergeSort(arr, mid + 1, right, lower, upper)
    let num = leftNum + rightNum

    // 统计右边减左边符合的下标对数量
    let i = left;
    let l = mid + 1;
    let r = mid + 1;
    while (i <= mid) {
    
    
       while (l <= right && arr[l] - arr[i] < lower) l++;
       while (r <= right && arr[r] - arr[i] <= upper) r++;
       num += (r - l);
       i++;
    }

    // 将左右两个有序数组合并
    let p1 = left, p2 = mid + 1, saveArr = [];
    while (p1 <= mid || p2 <= right) {
    
    
        if ((p2 > right) || (p1 <= mid && arr[p1] < arr[p2])) {
    
    
            saveArr.push(arr[p1])
            p1++
        } else {
    
    
            saveArr.push(arr[p2])
            p2++
        }
    }

    // 将合并后数组覆盖原数组
    for (let i = left; i <= right; i++) {
    
    
        arr[i] = saveArr[i - left]
    }

    return num
}

8. 現在の要素よりも小さい右側の要素の数を計算します。

現在の要素よりも小さい右側の要素の数を計算します。

一連の考え

まず、並べ替え規則は降順であるため、左側の配列要素が結果の配列に追加されると、右側の配列の残りの要素の数は、右側の配列の要素の数よりも大きい現在の要素の数になります。
注: 並べ替えにより元の配列要素の位置が変更されるため、要素の値と要素の元の位置を記録する新しいオブジェクト配列を作成してから、オブジェクト配列を並べ替える必要があります。

コード

/**
 * @param {number[]} nums
 * @return {number[]}
 */
var countSmaller = function (nums) {
    
    
    // 新建对象数组,记录元素原位置
    let objList = nums.map((i, inx) => {
    
    
        return {
    
    
            value: i,
            index: inx
        }
    })

    let resArr = new Array(nums.length).fill(0)
    mergeSort(objList, 0, nums.length - 1, resArr)

    return resArr
};

var mergeSort = function (arr, left, right, resArr) {
    
    
    if (left >= right) return;

    let mid = Math.floor((left + right) / 2);
    mergeSort(arr, left, mid, resArr)
    mergeSort(arr, mid + 1, right, resArr)

    //每次合并都记录个数
    let p1 = left, p2 = mid + 1, saveArr = [];
    while (p1 <= mid && p2 <= right) {
    
    
        if (arr[p1].value > arr[p2].value) {
    
    
            saveArr.push(arr[p1])
            //左数组加入结果数组时记录元素个数
            resArr[arr[p1].index] += (right - p2 + 1)
            p1++
        } else {
    
    
            saveArr.push(arr[p2])
            p2++
        }
    }

    if (p1 <= mid) {
    
    
        saveArr.push(...arr.slice(p1, mid + 1))
    } else if (p2 <= right) {
    
    
        saveArr.push(...arr.slice(p2, right + 1))
    }

    //将合并后数组覆盖原数组
    for (let i = left; i <= right; i++) {
    
    
        arr[i] = saveArr[i - left]
    }
}

おすすめ

転載: blog.csdn.net/m0_49343686/article/details/118678971