前端算法刷题之路—归并排序(8个题目详细分析)

归并排序 = 递归 + 合并

代码实现

通过递归将大数组拆分成每一项,然后对两项进行有序合并。归并排序中重点为合并,即将两个有序数组合并为一个有序数组。
我们可以设置两个指针,一个指向左边数组的第一项,一个指向右边数组第一项,比较两个指针对应的数,将小的放入临时数组中,然后对应小的指针右移,不断循环直到两个指针都移到对应数组的末尾后。
最后把临时数组覆盖到原数组对应位置就可以了。

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.逆序对问题

逆序对问题

思路

此题重点在于合并过程中记录逆序对的数量
首先对数组进行降序排列,在合并两个数组过程中,每当左边数组的值放入临时数组时,由于两边的数组都是降序,所以当前值比右边数组指针下标到数组末尾的所有值都大。即右边数组末尾下标 - 右指针下标 = 逆序对个数

代码

/**
 * @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.排序链表

排序链表

思路

这题就是普通的递归排序,只不过从数组变成链表。
个人感觉在拆分上难一点,一开始我是通过遍历一遍获得链表长度,然后再让指针跑到一半的位置,需要遍历两次。后来看到别人的快慢指针感觉不错就抄了:)
而合并过程没什么好说的,就是指针的左右横跳。

代码

/**
* 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.排列两棵二叉搜索树中的所有元素

两棵二叉搜索树中的所有元素

思路

由于二叉搜索树的特性:左节点比根节点小,右节点比根节点大
所以对二叉树进行中序遍历就会得到一个有序数组,然后对两个有序数组合并即可。

代码

/**
 * 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.共同祖先

共同祖先

思路

一共三种情况:

  1. p, q有一个是root;
  2. p, q分别在root左节点,右节点上;
  3. p, q都在root的同一侧;

代码

/**
 * @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.最深叶子节点的和

最深叶子节点的和

思路

记录两个值:

  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