299.猜数字游戏

目标

你在和朋友一起玩 猜数字(Bulls and Cows)游戏,该游戏规则如下:

写出一个秘密数字,并请朋友猜这个数字是多少。朋友每猜测一次,你就会给他一个包含下述信息的提示:

  • 猜测数字中有多少位属于数字和确切位置都猜对了(称为 "Bulls",公牛),
  • 有多少位属于数字猜对了但是位置不对(称为 "Cows",奶牛)。也就是说,这次猜测中有多少位非公牛数字可以通过重新排列转换成公牛数字。

给你一个秘密数字 secret 和朋友猜测的数字 guess ,请你返回对朋友这次猜测的提示。

提示的格式为 "xAyB" ,x 是公牛个数, y 是奶牛个数,A 表示公牛,B 表示奶牛。

请注意秘密数字和朋友猜测的数字都可能含有重复数字。

示例 1:

输入:secret = "1807", guess = "7810"
输出:"1A3B"
解释:数字和位置都对(公牛)用 '|' 连接,数字猜对位置不对(奶牛)的采用斜体加粗标识。
"1807"
  |
"7810"

示例 2:

输入:secret = "1123", guess = "0111"
输出:"1A1B"
解释:数字和位置都对(公牛)用 '|' 连接,数字猜对位置不对(奶牛)的采用斜体加粗标识。
"1123"        "1123"
  |      or     |
"0111"        "0111"
注意,两个不匹配的 1 中,只有一个会算作奶牛(数字猜对位置不对)。通过重新排列非公牛数字,其中仅有一个 1 可以成为公牛数字。

说明:

  • 1 <= secret.length, guess.length <= 1000
  • secret.length == guess.length
  • secret 和 guess 仅由数字组成

思路

这个题目的算法还是挺直接的,就是比较相应位置上的数字是否一致,如果一致bulls加1,否则就要累加错误数字的出现次数,但最多不能超过其在secret中的个数。

代码

/**
 * @date 2024-03-10 0:36
 */
public class GetHint {
    public String getHint(String secret, String guess) {
        int bulls = 0;
        int cows = 0;
        char[] secretCharArray = secret.toCharArray();
        int[] missArray = new int[10];
        int[] cowsArray = new int[10];
        for (int i = 0; i < secretCharArray.length; i++) {
            int guessCharacter = guess.charAt(i) - '0';
            int secretCharacter = secretCharArray[i] - '0';
            if (guessCharacter == secretCharacter) {
                bulls++;
            } else {
                missArray[secretCharacter] += 1;
                cowsArray[guessCharacter] += 1;
            }
        }
        for (int i = 0; i < cowsArray.length; i++) {
            if (missArray[i] > 0 && cowsArray[i] > 0) {
                cows += Math.min(missArray[i], cowsArray[i]);
            }
        }
        StringBuilder sb = new StringBuilder();
        return sb.append(bulls).append('A').append(cows).append('B').toString();
    }
}

这里面有几点需要注意:

  1. 数组只需要保留0-9十个数字即可
  2. 数字字符减去'0'可以得到对应的整型数字
  3. 不要使用加号拼接返回的结果,因为每一次字符串的修改都会创建新的对象,会显著影响性能

性能

2917.找出数组中的K-or值

目标

给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。

nums 中的 K-or 是一个满足以下条件的非负整数:

只有在 nums 中,至少存在 k 个元素的第 i 位值为 1 ,那么 K-or 中的第 i 位的值才是 1 。

返回 nums 的 K-or 值。

注意 :对于整数 x ,如果 (2^i AND x) == 2^i ,则 x 中的第 i 位值为 1 ,其中 AND 为按位与运算符。

示例 1:

输入:nums = [7,12,9,8,9,15], k = 4
输出:9
解释:nums[0]、nums[2]、nums[4] 和 nums[5] 的第 0 位的值为 1 。
nums[0] 和 nums[5] 的第 1 位的值为 1 。
nums[0]、nums[1] 和 nums[5] 的第 2 位的值为 1 。
nums[1]、nums[2]、nums[3]、nums[4] 和 nums[5] 的第 3 位的值为 1 。
只有第 0 位和第 3 位满足数组中至少存在 k 个元素在对应位上的值为 1 。因此,答案为 2^0 + 2^3 = 9 。

示例 2:

输入:nums = [2,12,1,11,4,5], k = 6
输出:0
解释:因为 k == 6 == nums.length ,所以数组的 6-or 等于其中所有元素按位与运算的结果。因此,答案为 2 AND 12 AND 1 AND 11 AND 4 AND 5 = 0 。

示例 3:

输入:nums = [10,8,5,9,11,6,8], k = 1
输出:15
解释:因为 k == 1 ,数组的 1-or 等于其中所有元素按位或运算的结果。因此,答案为 10 OR 8 OR 5 OR 9 OR 11 OR 6 OR 8 = 15 。

说明:

  • 1 <= nums.length <= 50
  • 0 <= nums[i] < 2^31
  • 1 <= k <= nums.length

思路

这个目标看起来有些难以理解,其实简单来说就是要我们返回一个int类型的数字,这个数字的每一bit是由数组元素相应bit的值共同决定的。如果数组中在该bit位上为1的元素个数超过k,那么就将结果值的相应bit位置1,否则置0。

现在问题转化为累加数组元素在某一bit位的值,然后与k比较来确定输出结果相应bit的值。可以使用移位运算来判断数字在某一特定bit的值是否为1,例如:数字7的低四位为 0111,想要判断第4个bit(从右边开始数)是否为1,可以将其右移3位,然后与1按位与即可。因为我们要判断的bit位经过右移变成了第一位,并且数字1只有第一位为1,其余位为0。

代码

/**
 * @date 2024-03-06 0:26
 */
public class FindKOr {

    public int findKOr_v1(int[] nums, int k) {
        int res = 0;
        for (int i = 0; i < 31; i++) {
            int counter = 0;
            for (int num : nums) {
               // 判断可以省去,提高效率
//                if ((num >> i & 1) == 1) {
//                    counter++;
//                }
                counter += num >> i & 1;
            }
            if (counter >= k) {
                res |= 1 << i;
            }
        }
        return res;
    }
}

性能

2834.找出美丽数组的最小和

目标

给你两个正整数:n 和 target 。

如果数组 nums 满足下述条件,则称其为 美丽数组 。

  • nums.length == n.
  • nums 由两两互不相同的正整数组成。
  • 在范围 [0, n-1] 内,不存在 两个 不同 下标 i 和 j ,使得 nums[i] + nums[j] == target 。

返回符合条件的美丽数组所可能具备的 最小 和,并对结果进行取模 10^9 + 7。

示例 1:

输入:n = 2, target = 3
输出:4
解释:nums = [1,3] 是美丽数组。
- nums 的长度为 n = 2 。
- nums 由两两互不相同的正整数组成。
- 不存在两个不同下标 i 和 j ,使得 nums[i] + nums[j] == 3 。
可以证明 4 是符合条件的美丽数组所可能具备的最小和。

示例 2:

输入:n = 3, target = 3
输出:8
解释:
nums = [1,3,4] 是美丽数组。 
- nums 的长度为 n = 3 。 
- nums 由两两互不相同的正整数组成。 
- 不存在两个不同下标 i 和 j ,使得 nums[i] + nums[j] == 3 。
可以证明 8 是符合条件的美丽数组所可能具备的最小和。

示例 3:

输入:n = 1, target = 1
输出:1
解释:nums = [1] 是美丽数组。

说明:

  • 1 <= n <= 10*9
  • 1 <= target <= 10^9

思路

首先,美丽数组一个长度为n的数组,数组中的元素均不相同且为正整数,且任意两个元素之后不能等于target。

我们需要寻找所有可能的美丽数组中,所有元素和最小的那一个。并返回这个最小和。

如何才能使元素和最小?

很明显应该是从1开始的正整数序列,但是这个序列中的值相加不能等于target。

那么本质上是要求 1______mid~~~~~~target______这一序列的值,其中~~~~~~区间的值应舍去,因为会与前面的相加得到target。

刚开始想的是循环遍历 [1, n) 累加和,并记录target - i,如果遍历的过程中遇到了就直接跳过,但是提交后提示超出内存限制。于是进行修改,不保存不符合条件的元素,直接根据下标来判断,这次提示超时。

然后尝试不使用循环,直接使用等差数列求和公式计算,结果整型溢出。需要注意运算符优先级高的会先计算,如果没有显式地类型转换就会溢出,并且隐式类型转化使用的是溢出后的值。

代码

/**
 * @date 2024-03-08 9:00
 */
public class MinimumPossibleSum {

    public final int MOD = 1000000007;

    public int minimumPossibleSum(int n, int target) {
        int mid;
        mid = target / 2;
        if (n <= mid) {
            return (int) ((n + n * (n - 1L) / 2L) % MOD);
        } else {
            int r = n - mid;
            return (int) (((mid + mid * (mid - 1L) / 2L) + (long) target * r + r * (r - 1L) / 2L) % MOD);
        }
    }
}

性能

2575.找出字符串的可整除数组

给你一个下标从 0 开始的字符串 word ,长度为 n ,由从 0 到 9 的数字组成。另给你一个正整数 m 。

word 的 可整除数组 div 是一个长度为 n 的整数数组,并满足:

  • 如果 word[0,...,i] 所表示的 数值 能被 m 整除,div[i] = 1
  • 否则,div[i] = 0
  • 返回 word 的可整除数组。

示例 1:

输入:word = "998244353", m = 3
输出:[1,1,0,0,0,1,1,0,0]
解释:仅有 4 个前缀可以被 3 整除:"9"、"99"、"998244" 和 "9982443" 。

示例 2:

输入:word = "1010", m = 10
输出:[0,1,0,1]
解释:仅有 2 个前缀可以被 10 整除:"10" 和 "1010" 。

说明:

  • 1 <= n <= 10^5
  • word.length == n
  • word 由数字 0 到 9 组成
  • 1 <= m <= 10^9

思路

题目要求的是从字符串第一个数字开始,验证数字能否被给定的正整数m整除。很容易想到的方法是创建一个StringBuilder,遍历字符串的Character,依次加入StringBuilder,然后转化为数字,求余判断能否整除。提交运行发现会溢出,虽然word的最大长度为10^5,但是Long的最大值也才 9223372036854775807 共19位,直接求解行不通。

很自然地想到需要分治处理,比方说先在Long的范围内算,每次处理19位,然后记录余数,再接上后面的18位。但是感觉这样具体实现起来很别扭,实际上求余数的时候也没必要每次都从头开始。

因此,我们可以用每一位对m(它不超过10^9,在Integer 2147483647 的范围内)进行MOD运算,同时记录结果。如果为0,则将输出数组中的相应位置赋值为1,否则为0。然后将余数 remainder * 10加上下一位字符所示的数字,再进行MOD运算以此类推即可。

代码

/**
 * @date 2024-03-07 9:27
 */
public class DivisibilityArray {
    public int[] divisibilityArray(String word, int m) {
        long remainder = 0;
        int n = word.length();
        int[] res = new int[n];
        for (int i = 0; i < n; i++) {
            remainder = (remainder * 10 + (word.charAt(i) - '0')) % m;
            if (remainder == 0) {
                res[i] = 1;
            }
        }
        return res;
    }

    public static void main(String[] args) {
        DivisibilityArray main = new DivisibilityArray();
        String str = "1000000000100000000030199999999610000000009199999999079999999938299999999010000000009999999991100000000010000000001000000000100000000071999999992100000000010000000003099999999759999999951000000000100000000010000000001000000000822999999988269999999921000000000100000000010000000005999999995900999999991100000000069999999947445979999999641999999999059999999957999999993100000000080609999999868999999992599999999867999999993100000000";
        int m = 1000000000;
        main.divisibilityArray(str, m);
    }
}

这里面有个小技巧,就是使用 Character - '0'来得到相应的数字,Character.getNumericValue考虑的情况比较多,这里用不到。同时需要注意,余数应使用long类型防止整数溢出。

性能

2368.受限条件下可到达节点的数目

目标

现有一棵由 n 个节点组成的无向树,节点编号从 0 到 n - 1 ,共有 n - 1 条边。

给你一个二维整数数组 edges ,长度为 n - 1 ,其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条边。另给你一个整数数组 restricted 表示 受限 节点。

在不访问受限节点的前提下,返回你可以从节点 0 到达的 最多 节点数目。

注意,节点 0 不 会标记为受限节点。

思路

自然的想法是构建图,将受限节点从中删除,然后深度优先遍历,同时记录节点个数。这里构建的图主要是为了获取其连通节点进行dfs,HashSet不太适合。因为数据可能并不是连续存储的,要先计算元素的Hash值,然后从桶中取出链表或者红黑树,才能找到元素。在本例中,性能会下降一倍。

代码

/**
 * @date 2024-03-02 15:39
 */
public class ReachableNodes {
    public int res = 1;
    boolean[] isRestricted;

    public int reachableNodes(int n, int[][] edges, int[] restricted) {
        List<Integer>[] g = new ArrayList[edges.length + 1];
        isRestricted = new boolean[edges.length + 1];
        for (int i : restricted) {
            isRestricted[i] = true;
        }
        for (int i = 0; i < g.length; i++) {
            g[i] = new ArrayList<>();
        }
        for (int[] edge : edges) {
            if (isRestricted[edge[0]] || isRestricted[edge[1]]) {
                continue;
            }
            g[edge[0]].add(edge[1]);
            g[edge[1]].add(edge[0]);
        }
        dfs(0, -1, g);
        return res;
    }

    public void dfs(int root, int parent, List<Integer>[] g) {
        for (Integer n : g[root]) {
            if (n == parent) {
                continue;
            }
            res++;
            dfs(n, root, g);
        }
    }
}

性能

看了官网的答案还可以使用并查集,耗时只要10ms,有时间可以看看。

232.用栈实现队列

目标

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:

  • 你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

示例 1:

输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]

输出:
[null, null, null, 1, 1, false]

解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false

说明:

  • 1 <= x <= 9
  • 最多调用 100 次 push、pop、peek 和 empty
  • 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)

进阶:

你能否实现每个操作均摊时间复杂度为 O(1) 的队列?换句话说,执行 n 个操作的总时间复杂度为 O(n) ,即使其中一个操作可能花费较长时间。

思路

这道题是让我们仅使用push压入栈顶、peek获取栈顶元素、pop弹栈等API实现先进先出队列。

关键是要想明白一点,入栈与出栈操作的是不同的栈,出栈s2中的元素是从入栈s1中获取的,并且,只能在s2为空的时候,将s1中的元素一个个弹栈再入s2,这样才能保证顺序反转。

代码

import java.util.ArrayDeque;
import java.util.Deque;

/**
 * @date 2024-03-04 0:42
 */
public class MyQueue {
    private final Deque<Integer> s1;
    private final Deque<Integer> s2;

    public MyQueue() {
        s1 = new ArrayDeque<>();
        s2 = new ArrayDeque<>();
    }

    public void push(int x) {
        s1.push(x);
    }

    public int pop() {
        if (s2.isEmpty()) {
            while (!s1.isEmpty()) {
                s2.push(s1.pop());
            }
        }
        return s2.pop();
    }

    public int peek() {
        if (s2.isEmpty()) {
            while (!s1.isEmpty()) {
                s2.push(s1.pop());
            }
        }
        if (s2.isEmpty()) {
            throw new RuntimeException();
        }
        return s2.peek();
    }

    public boolean empty() {
        return s2.isEmpty() && s1.isEmpty();
    }
}

Stack注释中有这么一段话,告诉我们应该优先使用Deque接口及其实现。

A more complete and consistent set of LIFO stack operations is provided by the Deque interface and its implementations, which should be used in preference to this class.

For example: Deque<Integer> stack = new ArrayDeque<Integer>();

在Java中,Stack类是基于Vector实现的,而Vector是一个线程安全的、可动态增长的数组。虽然Stack也是数组实现的,但由于Vector的一些内部机制(例如,每次增长时都会分配更大的数组,并将旧数组的内容复制到新数组中),它可能在某些操作上不如ArrayDeque高效。

相比之下,ArrayDeque是基于循环数组实现的,它避免了不必要的内存分配和复制操作。循环数组意味着当数组的一端达到容量限制时,元素会从另一端开始填充,从而充分利用了数组空间。这种实现方式通常比Vector或Stack更高效。

因此,即使Stack也是数组实现的,但由于Deque(如ArrayDeque)使用了不同的内部机制和优化,它在某些情况下可能会提供更好的性能。

另外,值得注意的是,在Java中,Stack类已经被标记为遗留(legacy),不建议在新的代码中使用。相反,应该使用Deque接口及其实现,如ArrayDeque,因为它们提供了更完整、更一致的操作集合,并且通常具有更好的性能。

性能

时间复杂度:push(数组赋值),empty为O(1),pop与peek虽然涉及到移动数据,但只有在出栈为空的时候才执行,均摊后为O(1)。

空间复杂度为O(n)。

225.用队列实现栈

目标

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。

实现 MyStack 类:

void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

注意:

你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty 这些操作。
你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

示例:

输入:

["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]

输出:

[null, null, null, 2, 2, false]

解释:

MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False

说明:

1 <= x <= 9
最多调用100 次 push、pop、top 和 empty
每次调用 pop 和 top 都保证栈不为空

进阶:你能否仅用一个队列来实现栈。

思路

这道题的目的是只使用标准库的部分API实现栈,即后进先出。

Java中可以使用Queue接口,offer插入队尾,peek获取队首元素,poll从队首获取并删除元素。

官网给出了两种方法,一种是使用两个队列,offer总是将新元素放到空队列q中,然后将另一队列q1从头至尾放入q,这样就实现了顺序的反转。

例如,依次入栈1,2,3,4:

说明:队首在右边

offer(1):

q: 1

q1:
----------------------------
offer(2):

q: 1

q1: 2

q1.offer(q.poll()):

q: 

q1: 1 2
----------------------------
offer(3):

q: 3

q1: 1 2

while(!q1.isEmpty){q.offer(q1.poll())}:

q: 1 2 3

q1:
----------------------------
offer(4):

q: 1 2 3

q1: 4

while(!q.isEmpty){q1.offer(q.poll())}:

q:

q1: 1 2 3 4

另一种只用一个队列的方法是:每次offer前先记录队列数据数量n,然后再offer,之后将前面n个数依次poll并offer到队尾。

例如,依次入栈1,2,3,4:

说明:队首在右边

offer(1):

q: 1

----------------------------
n = 1

offer(2):

q: 2 1

p.offer(p.poll()):

q: 1 2

----------------------------
n = 2

offer(3):

q: 3 1 2

循环2次:p.offer(p.poll())

q: 1 2 3

----------------------------
n = 3

offer(4):

q: 4 1 2 3

循环3次:p.offer(p.poll())

q:1 2 3 4

性能

这两种方法的性能基本相同。

时间复杂度:入栈操作均为O(n),其余为O(1)。

空间复杂度:O(n)

121.买卖股票的最佳时机

目标

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0。

示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

说明:

  • 1 <= prices.length <= 105
  • 0 <= prices[i] <= 104

思路

虽然这是一道简单题,但是我也想了一个多小时。算法实现起来简单,但是算法的设计,如何得到满足条件的结果也不是那么直观。

说回这道题,要求得最大利润,我们都明白要低买高卖。难道这道题就是让求数组的最大值与最小值?显然不是。我们的目标是求取一个买入点,再其之后的某天卖出获得的利润最大。只能操作一次,可以看成是长线交易(后面还有一道可以多次操作的题)。

暴力解法是遍历股票价格数组,然后向后循环,求得最大利润。时间复杂度是O(n!),不可行。

思考下面的问题:

取得最大利润是否必须在最低点买入?不是,比如[2,8,1,6]

取得最大利润是否必须在最高点卖出?不是,比如[3,8,1,7]

但是,如果遇到了更低的价格,那么它一定是当前最佳的买入点。因为不管后续价格怎么波动,想要获得最大收益,被减数当然越小越好。

因此,问题可以转化为:任取一个买入点,向后遍历并记录最大利润,直到出现一个更低的买入点,在这之前的一段时间内,我们所选的就是最佳买入点,可以记录这段时间内的最大利润。然后以新的最佳买入点继续向后遍历并记录最大利润,直到一个新的最佳买入点出现。在这一过程中如果最大利润比之前的值更大就替换掉。这样我们就得到了问题的答案。

其实,一开始我并没有这么清晰的思路。只是跟着感觉走:

  1. 首先,如果遍历时发现股票价格是递减的,那么我们肯定不能买入。这里,我们需要计算后一天减去前一天的收益,如果连着都是负数,那么直接跳过。
  2. 然后,如果找到了第一个正收益的点,那么我们买入。之后,我们开始累加收益,这样累加得到的是当天到买入点的收益。即 如果a是我们的买入点,那么往后利润的累加和 profitSum(b - a) + (c - b) + (d - c) + (e - d) = e - a。在这一过程中,我们记录最大收益 profitMax
  3. 接下来,我们需要找到更低的买入点。如果我们的买入点是i,当我们遍历到第j天的时候,profitSum = prices[j] - prices[i],而当天与前一天的利润为 profit = prices[j] - prices[j-1],如果profit > profitSum,那么prices[j] - prices[j-1] > prices[j] - prices[i]prices[j-1] < prices[i]j-1 就是新的最佳买入点。这时我们需要将利润和重置,然后再比较最大利润。

最开始写的时候忽略了查找最新买入点的这个步骤,增加的这个判断 profit > profitSum 是根据错误案例调试出来的,感觉就是如果一天的收益就比之前的历史收益大了,那就没必要再累加前面的利润了,应该重新开始。

代码

/**
 * @date 2024-03-03 1:44
 */
public class MaxProfit {

    public int maxProfit(int[] prices) {
        int profitSum = 0;
        int profitMax = 0;
        for (int i = 1; i < prices.length; i++) {
            int profit = prices[i] - prices[i - 1];
            if (profitSum <= 0 && profit <= 0) {
                continue;
            }
            profitSum += profit;
            if (profit > profitSum) {
                //  最开始的时候没写这个判断
                profitSum = profit;
            }
            if (profitSum > profitMax) {
                profitMax = profitSum;
            }

        }
        return profitMax;
    }

    public static void main(String[] args) {
        MaxProfit main = new MaxProfit();
        System.out.println(main.maxProfit(new int[]{3, 3, 5, 0, 0, 3, 1, 4}));
    }

}

理清楚之后发现许多操作是没必要的,比如累加每天的利润,实际就是当天价格减去买入点价格。

    public int maxProfit(int[] prices) {
        int buyPoint = 0;
        int profitMax = 0;
        for (int i = 1; i < prices.length; i++) {
            int profit = prices[i] - prices[buyPoint];
            if (profit <= 0) {
                buyPoint = i;
            } else if (profit > profitMax) {
                profitMax = profit;
            }

        }
        return profitMax;
    }

性能

优化后

169.多数元素

目标

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入:nums = [3,2,3]
输出:3
示例 2:

输入:nums = [2,2,1,1,1,2,2]
输出:2

说明:

  • n == nums.length
  • 1 <= n <= 5 * 104
  • -109 <= nums[i] <= 109

进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。

思路

今天做个简单题吧。要求时间复杂度是O(n),那么就不能嵌套循环,空间复杂度度是O(1),也就不能开辟新数组。很自然的可以想到:累加当前出现次数 count 最多的元素 res,如果遇到其它元素则减1,当 count 为负数时,说明当前出现次数最多的元素 可能 发生改变,将res替换为当前元素,并将count置1。

这里说 可能,是因为只要遇到与所选元素值不相等的, count 就减1。我们并不清楚这些其它元素值是否都相同,只能够推出当初所选的多数被其它少数反超了。但是从总体来考虑,如果我们所选择的真是多数元素,那么它一定会在后面再次反超。

官网介绍了一种投票算法 Boyer-Moore,应该也是这种思路吧。

官网还给出了一种分治算法,主要思想是:如果将数组分成两部分,那么数组的众数至少是一部分的众数。递归求解,然后回溯合并,确定子数组众数。不过时间复杂度O(nlog⁡n),参考算法导论P53主定理。空间复杂度:O(log⁡n),递归用到的栈空间。

代码

/**
 * @date 2024-03-02 22:24
 */
public class MajorityElement {
    public int majorityElement(int[] nums) {
        int res = nums[0];
        int count = 1;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] != res) {
                count--;
                // 出错点:反超时应当将值设为1,参考错误用例[10,9,9,9,10]
                if (count < 0) {
                    res = nums[i];
                    count = 1;
                }
            } else {
                count++;
            }
            // 本以为加上可以提高性能,谁知道还慢了2ms
            // if (count > Math.floor(nums.length / 2.0)) {
            //     break;
            // }
        }
        return res;
    }

    public static void main(String[] args) {
        MajorityElement main = new MajorityElement();
        System.out.println(main.majorityElement(new int[]{10, 9, 9, 9, 10}));
    }
}

性能

本以为判断条件可以提高效率,谁知道还慢了2ms,耗时增加了2倍,因为每次算出答案基本上也都循环完了。