剑指Offer-分治算法部分

合并两个排序的链表

输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

示例1:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
限制:
0 <= 链表长度 <= 1000

解题思路1

递归分治

var mergeTwoLists = function(l1, l2) {
    if(l1 == null) return l2;
    if(l2 == null) return l1;
    if(l1.val < l2.val){
        l1.next = mergeTwoLists(l1.next,l2);
        return l1
    }else{
        l2.next = mergeTwoLists(l1,l2.next);
        return l2
    }
};

复杂度分析
时间复杂度:O(N),其中N为两个链表节点总数
空间复杂度:O(1)

解题思路2

迭代

var mergeTwoLists = function(l1, l2) {
    let current = new ListNode();
    let temp = current;
    while(l1 || l2){
        if(!l1){
            current.next = l2;
            return temp.next;
        }
        if(!l2){
            current.next = l1;
            return temp.next;
        }
        if(l1.val < l2.val){
            current.next = l1;
            l1 = l1.next;
        }else{
            current.next = l2;
            l2 = l2.next;
        }
        current = current.next;
    }
    return temp.next
};

复杂度分析
时间复杂度:O(N),其中N为两个链表节点总数
空间复杂度:加上栈空间的话,空间复杂度为 O(N),其中N为两个链表节点总数

二叉搜索树与双向链表

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
为了更好地理解问题,以下面的二叉搜索树为例:
在这里插入图片描述
我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点
在这里插入图片描述
特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。

解题思路1

递归+中序遍历
由题意得,二叉搜索树的左子树小与父节点小与右子树,因此我们首先需要得到最小值,也就是二叉搜索树最下面一层的左子树
结合中序遍历,递归处理二叉树。初始化一个代表上一个节点的 pre 变量。递归中要做的就是:pre 的 right 指针指向当前节点 node,node 的 left 指向 pre,并且将 pre 更新为 node。

var treeToDoublyList = function(root) {
    if (!root) {
        return;
    }
    let head = null;
    let pre = head;
    inorder(root);
    pre.right = head;
    head.left = pre;
    return head
    function inorder(node){
        if(!node){
            return; 
        }
        inorder(node.left);
        if(!pre){
            head = node;
        }else{
            pre.right = node;
        };
        node.left = pre;
        pre = node;
        inorder(node.right);
    }
};

解题思路2

非递归+中序遍历
转化思路是用栈来模拟递归调用的过程

var treeToDoublyList = function(root) {
    if (!root) {
        return;
    }

    const stack = [];
    let node = root;
    let pre = null;
    let head = null;
    while (stack.length || node) {
        if (node) {
            stack.push(node);
            node = node.left;
        } else {
            const top = stack.pop();
            if (!pre) {
                head = top;
            } else {
                pre.right = top;
            }
            top.left = pre;
            pre = top;

            node = top.right;
        }
    }

    head.left = pre;
    pre.right = head;
    return head;
};

数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:
输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2
限制:
1 <= 数组长度 <= 50000

解题思路1

先对数组进行排序,在遍历数组的同时利用indexOf和lastIndexOf的差值来找到这个数

var majorityElement = function(nums) {
    nums = nums.sort((a,b)=>a-b);
    for(let i=0;i<nums.length;){
        let len = nums.lastIndexOf(nums[i]) - nums.indexOf(nums[i]) + 1;
        if(len > Math.floor(nums.length/2)){
            return nums[i]
        }
        i = nums.lastIndexOf(nums[i]) + 1;
    }
};

解题思路2

摩尔投票
假如我们有个职位,需要从 A,B 两位候选人中选出
先抽出一张票,投的是 A,我们在黑板上写着当前胜利者:A,票数:1
再抽出一张票,投的是 A,我们在黑板上写着当前胜利者:A,票数:2
再抽出一张票,投的是 B,用一张 B 抵消一张 A 的选票,我们在黑板上写着当前胜利者:A,票数:1
再抽出一张票,投的是 B,用一张 B 抵消一张 A 的选票,我们在黑板上写着当前胜利者:无,票数:0
再抽出一张票,投的是 A,我们在黑板上写着当前胜利者:A,票数:1
抽取完毕,恭喜 A 获胜,赢得该职位。
经过以上实例分析,我们可以得出 3个要点:
1.不同候选人的选票之间,可以一一抵消。
2.若当前胜利者存在多张选票时,不同的候选人的票,只能抵消一张当前胜利者的票。
3.若当前双方的选票被抵消为零,下一次抽出的候选人,将作为暂时的胜利者领先。

var majorityElement = function(nums) {
    let ans = 0, count = 0;
    for(let i = 0; i < nums.length; i++){
        if(!count) {
            ans = nums[i];
            count++;
        }else count += nums[i] === ans ? 1 : -1;
    }
    return ans;
};

最小的k个数

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]
限制:
0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000

解题思路1

直接排序

var getLeastNumbers = function(arr, k) {
    return arr.sort((a, b) => a - b).slice(0, k);
};

时间复杂度是O(NlogN),空间复杂度是O(logN)

解题思路2

最大堆
堆是一种非常常用的数据结构。最大堆的性质是:节点值大于子节点的值,堆顶元素是最大元素。利用这个性质,整体的算法流程如下:
创建大小为 k 的最大堆
将数组的前 k 个元素放入堆中
从下标 k 继续开始依次遍历数组的剩余元素:
如果元素小于堆顶元素,那么取出堆顶元素,将当前元素入堆
如果元素大于/等于堆顶元素,不做操作
由于堆的大小是 K,空间复杂度是O(K),时间复杂度是O(NlogK)。

// ac地址:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/
// 原文地址:https://xxoo521.com/2020-02-21-least-nums/
function swap(arr, i, j) {
    [arr[i], arr[j]] = [arr[j], arr[i]];
}

class MaxHeap {
    constructor(arr = []) {
        this.container = [];
        if (Array.isArray(arr)) {
            arr.forEach(this.insert.bind(this));
        }
    }

    insert(data) {
        const { container } = this;

        container.push(data);
        let index = container.length - 1;
        while (index) {
            let parent = Math.floor((index - 1) / 2);
            if (container[index] <= container[parent]) {
                break;
            }
            swap(container, index, parent);
            index = parent;
        }
    }

    extract() {
        const { container } = this;
        if (!container.length) {
            return null;
        }

        swap(container, 0, container.length - 1);
        const res = container.pop();
        const length = container.length;
        let index = 0,
            exchange = index * 2 + 1;

        while (exchange < length) {
            // 如果有右节点,并且右节点的值大于左节点的值
            let right = index * 2 + 2;
            if (right < length && container[right] > container[exchange]) {
                exchange = right;
            }
            if (container[exchange] <= container[index]) {
                break;
            }
            swap(container, exchange, index);
            index = exchange;
            exchange = index * 2 + 1;
        }

        return res;
    }

    top() {
        if (this.container.length) return this.container[0];
        return null;
    }
}

/**
 * @param {number[]} arr
 * @param {number} k
 * @return {number[]}
 */
var getLeastNumbers = function(arr, k) {
    const length = arr.length;
    if (k >= length) {
        return arr;
    }

    const heap = new MaxHeap(arr.slice(0, k));
    for (let i = k; i < length; ++i) {
        if (heap.top() > arr[i]) {
            heap.extract();
            heap.insert(arr[i]);
        }
    }
    return heap.container;
};

参考作者:xin-tan
链接:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/chao-quan-3chong-jie-fa-zhi-jie-pai-xu-zui-da-dui-/

连续子数组的最大和

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。

示例1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
提示:
1 <= arr.length <= 10^5
-100 <= arr[i] <= 100

解题思路1

动态规划
定义状态数组dp[i]的含义:数组中元素下标为[0, i]的连续子数组最大和。
状态转移的过程如下:
初始情况:dp[0] = nums[0]
若 nums[i] > 0,那么 dp[i] = nums[i] + dp[i - 1]
若 nums[i] <= 0,那么 dp[i] = nums[i]

var maxSubArray = function(nums) {
    const dp = [];

    let res = (dp[0] = nums[0]);
    for (let i = 1; i < nums.length; ++i) {
        dp[i] = nums[i];
        if (dp[i - 1] > 0) {
            dp[i] += dp[i - 1];
        }
        res = Math.max(res, dp[i]);
    }
    return res;
};

时间复杂度和空间复杂度都是O(N)。

解题思路2

原地进行动态规划
解法 1 中开辟了 dp 数组。其实在原数组上做修改,用nums[i]来表示dp[i]。所以解法 1 的代码可以优化为:

var maxSubArray = function(nums) {
    let res = nums[0];
    for (let i = 1; i < nums.length; ++i) {
        if (nums[i - 1] > 0) {
            nums[i] += nums[i - 1];
        }
        res = Math.max(res, nums[i]);
    }
    return res;
};

空间复杂度为O(1),时间复杂度为O(N)

解题思路3

本题的贪心法的思路是:在循环中找到不断找到当前最优的和 sum。

var maxSubArray = function(nums) {
    let maxSum = (sum = nums[0]);
    for (let i = 1; i < nums.length; ++i) {
        sum = Math.max(nums[i], sum + nums[i]);
        maxSum = Math.max(maxSum, sum);
    }
    return maxSum;
};

空间复杂度为O(1),时间复杂度为O(N)

解题思路4

分治法
分治法的做题思路是:先将问题分解为子问题;解决子问题后,再将子问题合并,解决主问题。
使用分治法解本题的思路是:
1.将数组分为 2 部分。例如 [1, 2, 3, 4] 被分为 [1, 2] 和 [3, 4]
2.通过递归计算,得到左右两部分的最大子序列和是 lsum,rsum
3.从数组中间开始向两边计算最大子序列和 cross
4.返回 max(lsum, cross, rsum)
在这里插入图片描述

function crossSum(nums, left, right, mid) {
    if (left === right) {
        return nums[left];
    }

    let leftMaxSum = Number.MIN_SAFE_INTEGER;
    let leftSum = 0;
    for (let i = mid; i >= left; --i) {
        leftSum += nums[i];
        leftMaxSum = Math.max(leftMaxSum, leftSum);
    }

    let rightMaxSum = Number.MIN_SAFE_INTEGER;
    let rightSum = 0;
    for (let i = mid + 1; i <= right; ++i) {
        rightSum += nums[i];
        rightMaxSum = Math.max(rightMaxSum, rightSum);
    }

    return leftMaxSum + rightMaxSum;
}
function __maxSubArray(nums, left, right) {
    if (left === right) {
        return nums[left];
    }

    const mid = Math.floor((left + right) / 2);
    const lsum = __maxSubArray(nums, left, mid);
    const rsum = __maxSubArray(nums, mid + 1, right);
    const cross = crossSum(nums, left, right, mid);

    return Math.max(lsum, rsum, cross);
}
var maxSubArray = function(nums) {
    return __maxSubArray(nums, 0, nums.length - 1);
};

时间复杂度是O(NlogN)。由于递归调用,所以空间复杂度是O(logN)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值