数据结构和算法三十五

剑指 Offer 57 - II. 和为 s 的连续正数序列

题目:输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

示例 1:
      输入:target = 9
      输出:[[2,3,4],[4,5]]
示例 2:
      输入:target = 15
      输出:[[1,2,3,4,5],[4,5,6],[7,8]]
限制:
      1 <= target <= 10^5
方法一:枚举 + 暴力
在这里插入图片描述

class Method1{
    public int[][] findContinuousSequence(int target) {
        List<int[]> vec = new ArrayList<int[]>();
        int sum = 0, limit = (target - 1) / 2; // (target - 1) / 2 等效于 target / 2 下取整
        for (int i = 1; i <= limit; ++i) {
            for (int j = i;; ++j) {
                sum += j;
                if (sum > target) {
                    sum = 0;
                    break;
                } else if (sum == target) {
                    int[] res = new int[j - i + 1];
                    for (int k = i; k <= j; ++k) {
                        res[k - i] = k;
                    }
                    vec.add(res);
                    sum = 0;
                    break;
                }
            }
        }
        return vec.toArray(new int[vec.size()][]);
    }
}

复杂度分析:
在这里插入图片描述
方法二:枚举 + 数学优化
在这里插入图片描述

class Method2{
    public int[][] findContinuousSequence(int target) {
        List<int[]> vec = new ArrayList<int[]>();
        int sum = 0, limit = (target - 1) / 2; // (target - 1) / 2 等效于 target / 2 下取整
        for (int x = 1; x <= limit; ++x) {
            long delta = 1 - 4 * (x - (long) x * x - 2 * target);
            if (delta < 0) {
                continue;
            }
            int delta_sqrt = (int) Math.sqrt(delta + 0.5);
            if ((long) delta_sqrt * delta_sqrt == delta && (delta_sqrt - 1) % 2 == 0) {
                int y = (-1 + delta_sqrt) / 2; // 另一个解(-1-delta_sqrt)/2必然小于0,不用考虑
                if (x < y) {
                    int[] res = new int[y - x + 1];
                    for (int i = x; i <= y; ++i) {
                        res[i - x] = i;
                    }
                    vec.add(res);
                }
            }
        }
        return vec.toArray(new int[vec.size()][]);
    }
}

复杂度分析:
在这里插入图片描述
方法三:双指针
在这里插入图片描述

在这里插入图片描述

class Method3{
    public int[][] findContinuousSequence(int target) {
        List<int[]> vec = new ArrayList<int[]>();
        for (int l = 1, r = 2; l < r;) {
            int sum = (l + r) * (r - l + 1) / 2;
            if (sum == target) {
                int[] res = new int[r - l + 1];
                for (int i = l; i <= r; ++i) {
                    res[i - l] = i;
                }
                vec.add(res);
                l++;
            } else if (sum < target) {
                r++;
            } else {
                l++;
            }
        }
        return vec.toArray(new int[vec.size()][]);
    }
}

复杂度分析:
在这里插入图片描述
方法三:滑动窗口(双指针)
在这里插入图片描述

算法流程:
在这里插入图片描述

复杂度分析:
在这里插入图片描述

当 target = 9 时,以上求解流程如下图所示:

在这里插入图片描述

代码:

观察算法流程发现,当 s = target 和 s > target 的移动边界操作相同,因此可以合并。

class Method3{
    public int[][] findContinuousSequence(int target) {
        int i = 1, j = 2, s = 3;
        List<int[]> res = new ArrayList<>();
        while(i < j) {
            if(s == target) {
                int[] ans = new int[j - i + 1];
                for(int k = i; k <= j; k++)
                    ans[k - i] = k;
                res.add(ans);
            }
            if(s >= target) {
                s -= i;
                i++;
            } else {
                j++;
                s += j;
            }
        }
        return res.toArray(new int[0][]);
    }
}

方法四:求和公式
在这里插入图片描述

在这里插入图片描述

当 target = 9 时,以上求解流程如下图所示:

class Method4{
    public int[][] findContinuousSequence(int target) {
        int i = 1;
        double j = 2.0;
        List<int[]> res = new ArrayList<>();
        while(i < j) {
            j = (-1 + Math.sqrt(1 + 4 * (2 * target + (long) i * i - i))) / 2;
            if(i < j && j == (int)j) {
                int[] ans = new int[(int)j - i + 1];
                for(int k = i; k <= (int)j; k++)
                    ans[k - i] = k;
                res.add(ans);
            }
            i++;
        }
        return res.toArray(new int[0][]);
    }
}

剑指 Offer 62. 圆圈中最后剩下的数字

题目:0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
示例 1:
      输入: n = 5, m = 3
      输出: 3
示例 2:
      输入: n = 10, m = 17
      输出: 2
限制:
      1 <= n <= 10^5
      1 <= m <= 10^6
方法一:数学 + 递归

思路:
在这里插入图片描述
算法:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

class Method1{
    public int lastRemaining(int n, int m) {
        return f(n, m);
    }

    public int f(int n, int m) {
        if (n == 1) {
            return 0;
        }
        int x = f(n - 1, m);
        return (m + x) % n;
    }
}

复杂度分析:
在这里插入图片描述
方法二:数学 + 迭代

思路与算法:
上面的递归可以改写为迭代,避免递归使用栈空间。

class Method2{
    public int lastRemaining(int n, int m) {
        int f = 0;
        for (int i = 2; i != n + 1; ++i) {
            f = (m + f) % i;
        }
        return f;
    }
}

复杂度分析:
在这里插入图片描述


解题思路:

这个问题是以弗拉维奥·约瑟夫命名的,他是1世纪的一名犹太历史学家。他在自己的日记中写道,他和他的40个战友被罗马军队包围在洞中。他们讨论是自杀还是被俘,最终决定自杀,并以抽签的方式决定谁杀掉谁。约瑟夫斯和另外一个人是最后两个留下的人。约瑟夫斯说服了那个人,他们将向罗马军队投降,不再自杀。约瑟夫斯把他的存活归因于运气或天意,他不知道是哪一个。 —【约瑟夫问题】维基百科

方法三:模拟链表,O(n^2)
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

class Method3{
    public int lastRemaining(int n, int m) {
        ArrayList<Integer> list = new ArrayList<>(n);
        for (int i = 0; i < n; i++) {
            list.add(i);
        }
        int idx = 0;
        while (n > 1) {
            idx = (idx + m - 1) % n;
            list.remove(idx);
            n--;
        }
        return list.get(0);
    }
}

方法四:数学解法,O(n)
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

class Method4{
    public int lastRemaining(int n, int m) {
        int ans = 0;
        // 最后一轮剩下2个人,所以从2开始反推
        for (int i = 2; i <= n; i++) {
            ans = (ans + m) % i;
        }
        return ans;
    }
}

方法五:动态规划

解题思路:
在这里插入图片描述

实际上,本题是著名的 “约瑟夫环” 问题,可使用 动态规划 解决。

在这里插入图片描述

请注意,数字环是 首尾相接 的,为方便行文,本文使用列表形式表示。

在这里插入图片描述

在这里插入图片描述

以 n = 5, m = 3 的示例如下图所示。

在这里插入图片描述

在这里插入图片描述

以上数学推导本质是得出动态规划的 转移方程 和 初始状态 。

动态规划解析:
在这里插入图片描述

如下图所示,为 n = 5, m = 3 时的状态转移和对应的模拟删除过程

在这里插入图片描述
复杂度分析:
在这里插入图片描述

代码:
根据状态转移方程的递推特性,无需建立状态列表 dp,而使用一个变量 x 执行状态转移即可。

class Method5{
    public int lastRemaining(int n, int m) {
        int x = 0;
        for (int i = 2; i <= n; i++) {
            x = (x + m) % i;
        }
        return x;
    }
}

总结:

      今天恐怕是更新博客最晚的一次了,同时也是写的字数最多的一次!酸酸的520和521都过去了!终于逃离了吃狗粮了,哈哈~~~!
      明天晚上就剩剑指Offer最后的三道题了,准备一次性将它们更新完!后续会出一些学习路径和方法!可能有前端的、后端的、测试的、运维的,以及其他励志类的文章等等!敬请期待哦!
      每天学习一点点,幸福生活每一天!或许别人都已经进入梦乡,孤军奋战的我们依旧在努力学习,一天、一周、一月、一年,差距慢慢就拉开了!既然上天没有赋予我们聪明的大脑,那我们就用加倍的努力去弥补我们自身的不足!
      最后,愿我们都能在各行各业中能够取得不同的成就,不负亲人、朋友、老师、长辈和国家的期望!能够用自身的所学知识为国家贡献出自己的一份力量!一起加油!
                                                                                                                       2021年5月21日夜

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值