怒刷LeetCode的第15天(Java版)

目录

第一题

题目来源

题目内容

解决方法

方法一:哈希表+双向链表

方法二:TreeMap

方法三:双哈希表

第二题

题目来源

题目内容

解决方法

方法一:二分查找

方法二:线性搜索

方法三:Arrays类的binarySearch方法

方法四:插入排序

第三题

题目来源

题目内容

解决方法

方法一:二维数组

方法二:哈希集合

方法三:单一遍历

方法四:位运算


第一题

题目来源

460. LFU 缓存 - 力扣(LeetCode)

题目内容

解决方法

方法一:哈希表+双向链表

LFU(Least Frequently Used)缓存算法的主要思想是根据键的使用频率来进行缓存项的管理。

首先,我们需要设计一个数据结构来存储缓存的键值对,并记录每个键的使用计数(即使用频率)。为了支持 O(1) 的时间复杂度,我们可以使用哈希表来存储键值对,并且使用双向链表来维护具有相同使用计数的键的顺序。

算法的关键步骤如下:

  1. 使用一个哈希表 values 来存储键值对,以支持快速的键值获取和更新操作。
  2. 使用另一个哈希表 frequencies 来记录每个键的使用计数。
  3. 使用一个哈希表 frequencyKeys 来记录具有相同使用计数的键的集合,并使用双向链表来维护它们的顺序。
  4. 使用一个变量 minFrequency 来记录当前最小的使用计数。

对于 get 操作:

  • 如果键不存在于缓存中,返回 -1。
  • 如果键存在于缓存中,需要执行以下操作:
    • 更新键的使用计数:将该键的使用计数加一。
    • 将该键从原来的使用计数对应的双向链表中移除。
    • 如果该键所在的双向链表为空且使用计数等于 minFrequency,则更新 minFrequency 为下一个更大的使用计数。
    • 将该键添加到新的使用计数对应的双向链表中,并更新 minFrequency 为 1(因为该键变成了最近被使用的键)。
    • 返回键对应的值。

对于 put 操作:

  • 如果容量已满且需要插入一个新的键值对时,需要执行以下操作:
    • 通过 frequencyKeys[minFrequency] 找到双向链表的头节点,得到需要移除的键,将其从缓存中和相应的哈希表中移除。
  • 如果键已经存在于缓存中,需要执行以下操作:
    • 更新键对应的值。
    • 执行 get 操作来更新键的使用计数和双向链表的顺序。
  • 如果键不存在于缓存中,需要执行以下操作:
    • 如果缓存已满,执行上述的容量已满的移除操作。
    • 插入新的键值对到缓存中。
    • 将该键的使用计数设置为 1。
    • 将该键添加到使用计数为 1 的双向链表中。
    • 更新 minFrequency 为 1。
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;class LFUCache {private int capacity;private int minFrequency;private Map<Integer, Integer> values;private Map<Integer, Integer> frequencies;private Map<Integer, LinkedHashSet<Integer>> frequencyKeys;public LFUCache(int capacity) {this.capacity = capacity;this.minFrequency = 1;this.values = new HashMap<>();this.frequencies = new HashMap<>();this.frequencyKeys = new HashMap<>();}public int get(int key) {if (!values.containsKey(key)) {return -1;}// 更新键的使用频率int frequency = frequencies.get(key);frequencies.put(key, frequency + 1);// 更新相应频率的键集合frequencyKeys.get(frequency).remove(key);if (frequency == minFrequency && frequencyKeys.get(frequency).isEmpty()) {minFrequency++;}frequencyKeys.computeIfAbsent(frequency + 1, k -> new LinkedHashSet<>()).add(key);return values.get(key);}public void put(int key, int value) {if (capacity <= 0) {return;}if (values.containsKey(key)) {// 更新键的值和使用频率values.put(key, value);get(key);return;}if (values.size() >= capacity) {// 移除最不经常使用的项int evictKey = frequencyKeys.get(minFrequency).iterator().next();values.remove(evictKey);frequencies.remove(evictKey);frequencyKeys.get(minFrequency).remove(evictKey);}// 插入新的键值对values.put(key, value);frequencies.put(key, 1);frequencyKeys.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(key);minFrequency = 1;}
}

复杂度分析:

对于 get 操作:

  • 在哈希表中查找键值对的时间复杂度为 O(1)。
  • 更新键的使用计数、移除和添加键到相应的双向链表的时间复杂度也为 O(1)。

对于 put 操作:

  • 在哈希表中插入键值对的时间复杂度为 O(1)。
  • 更新键的使用计数、移除和添加键到相应的双向链表的时间复杂度也为 O(1)。

因此,get 和 put 操作的时间复杂度都是 O(1)。

空间复杂度主要取决于存储缓存键值对和使用计数的数据结构:

  • 哈希表 `values` 存储键值对,占用的空间为 O(capacity),其中 capacity 是缓存的最大容量。
  • 哈希表 `frequencies` 存储键的使用计数,占用的空间为 O(capacity)。
  • 哈希表 `frequencyKeys` 存储具有相同使用计数的键的集合,占用的空间为 O(capacity)。
  • 双向链表的节点数等于缓存中的键数,最多为 capacity。

因此,LFU 缓存算法的空间复杂度为 O(capacity)。

需要注意的是,以上复杂度分析是基于假设哈希表的操作时间复杂度为 O(1) 的情况。在实际应用中,哈希表的性能也受到哈希函数的质量和哈希冲突的处理等因素影响。此外,LFU 缓存算法本身的性能也与具体的使用场景和数据访问模式相关。因此,在实际应用中,需要综合考虑实际情况来评估算法的性能。

LeetCode运行结果:

方法二:TreeMap

import java.util.*;class LFUCache {private int capacity;private Map<Integer, Integer> cache; // 存储键值对private Map<Integer, Integer> frequencies; // 存储键的使用计数private TreeMap<Integer, LinkedHashSet<Integer>> frequencyKeys; // 存储具有相同使用计数的键的集合public LFUCache(int capacity) {this.capacity = capacity;this.cache = new HashMap<>();this.frequencies = new HashMap<>();this.frequencyKeys = new TreeMap<>();}public int get(int key) {if (cache.containsKey(key)) {updateFrequency(key);return cache.get(key);}return -1;}public void put(int key, int value) {if (capacity <= 0) return;if (cache.containsKey(key)) {cache.put(key, value);updateFrequency(key);} else {if (cache.size() >= capacity) {removeLFUKey();}cache.put(key, value);frequencies.put(key, 1);addToFrequencyKeys(key, 1);}}private void updateFrequency(int key) {int frequency = frequencies.get(key);frequencies.put(key, frequency + 1);removeFromFrequencyKeys(key, frequency);addToFrequencyKeys(key, frequency + 1);}private void removeLFUKey() {LinkedHashSet<Integer> keysWithMinFreq = frequencyKeys.firstEntry().getValue();int lfuKey = keysWithMinFreq.iterator().next();keysWithMinFreq.remove(lfuKey);if (keysWithMinFreq.isEmpty()) {frequencyKeys.remove(frequencyKeys.firstKey());}cache.remove(lfuKey);frequencies.remove(lfuKey);}private void addToFrequencyKeys(int key, int frequency) {frequencyKeys.computeIfAbsent(frequency, k -> new LinkedHashSet<>()).add(key);}private void removeFromFrequencyKeys(int key, int frequency) {LinkedHashSet<Integer> keysWithFreq = frequencyKeys.get(frequency);if (keysWithFreq != null) {keysWithFreq.remove(key);if (keysWithFreq.isEmpty()) {frequencyKeys.remove(frequency);}}}
}

以上代码中,cache 是存储键值对的哈希表,frequencies 是存储键的使用计数的哈希表,frequencyKeys 是存储具有相同使用计数的键的集合的有序映射(基于 TreeMap 实现)。 

复杂度分析:

时间复杂度分析:

  • LFUCache(int capacity) 构造函数的时间复杂度为 O(1),只是对私有变量进行初始化。
  • get(int key) 方法的时间复杂度为 O(1)。通过 HashMap 直接访问键值对,时间复杂度为常数级别。
  • put(int key, int value) 方法的时间复杂度为 O(1)。通过 HashMap 直接插入或更新键值对,时间复杂度为常数级别。
  • updateFrequency(int key) 方法的时间复杂度为 O(log M),其中 M 表示不同频率的数量。在更新某个键的使用计数时,需要先获取当前计数,然后进行加一操作,并将该键从旧的频率集合中删除,再将其添加到新的频率集合中。由于使用的是 TreeMap,获取和删除操作的时间复杂度为 O(log M)。
  • removeLFUKey() 方法的时间复杂度为 O(log M),其中 M 表示不同频率的数量。通过 TreeMap 的 firstEntry() 获取最小频率对应的 keys 集合的时间复杂度为 O(log M)。然后从集合中移除一个键和删除空集合的时间复杂度为 O(1)。因此,整个方法的时间复杂度为 O(log M)。

综上所述,根据给定的 LFU 缓存算法实现,主要操作的时间复杂度为 O(1) 和 O(log M),其中 M 表示不同频率的数量。

空间复杂度分析:

  • 存储键值对的 cache 使用的空间为 O(capacity),其中 capacity 是 LFU 缓存的容量。
  • 存储键的使用计数的 frequencies 使用的空间为 O(capacity),最坏情况下需要存储 capacity 个键的使用计数。
  • 存储具有相同使用计数的键的集合的 frequencyKeys 使用的空间为 O(capacity * maxFreq),其中 maxFreq 表示频率的最大值,即最高使用计数。每个键和频率都需要占用常数级别的空间。

综上所述,根据给定的 LFU 缓存算法实现,总体的空间复杂度为 O(capacity * maxFreq)。

LeetCode运行结果:

方法三:双哈希表

import java.util.*;class LFUCache {private int capacity;private Map<Integer, Integer> cache; // 存储键值对private Map<Integer, Integer> frequencies; // 存储键的使用计数private Map<Integer, LinkedHashSet<Integer>> frequencyKeys; // 存储具有相同使用计数的键的集合public LFUCache(int capacity) {this.capacity = capacity;this.cache = new HashMap<>();this.frequencies = new HashMap<>();this.frequencyKeys = new HashMap<>();}public int get(int key) {if (cache.containsKey(key)) {updateFrequency(key);return cache.get(key);}return -1;}public void put(int key, int value) {if (capacity <= 0) return;if (cache.containsKey(key)) {cache.put(key, value);updateFrequency(key);} else {if (cache.size() >= capacity) {removeLFUKey();}cache.put(key, value);frequencies.put(key, 1);addToFrequencyKeys(key, 1);}}private void updateFrequency(int key) {int frequency = frequencies.get(key);frequencies.put(key, frequency + 1);removeFromFrequencyKeys(key, frequency);addToFrequencyKeys(key, frequency + 1);}private void removeLFUKey() {int minFreq = Collections.min(frequencyKeys.keySet());LinkedHashSet<Integer> keysWithMinFreq = frequencyKeys.get(minFreq);int lfuKey = keysWithMinFreq.iterator().next();keysWithMinFreq.remove(lfuKey);if (keysWithMinFreq.isEmpty()) {frequencyKeys.remove(minFreq);}cache.remove(lfuKey);frequencies.remove(lfuKey);}private void addToFrequencyKeys(int key, int frequency) {frequencyKeys.computeIfAbsent(frequency, k -> new LinkedHashSet<>()).add(key);}private void removeFromFrequencyKeys(int key, int frequency) {LinkedHashSet<Integer> keysWithFreq = frequencyKeys.get(frequency);if (keysWithFreq != null) {keysWithFreq.remove(key);if (keysWithFreq.isEmpty()) {frequencyKeys.remove(frequency);}}}
}

这个代码实现了LFU缓存的基本思路和算法。

缓存的核心数据结构是一个双哈希表,其中包括三个部分:

  • cache:用于存储键值对的哈希表。通过键来查找对应的值。
  • frequencies:用于存储键的使用计数的哈希表。通过键来查找对应的使用计数。
  • frequencyKeys:用于存储具有相同使用计数的键的集合。使用键的使用计数作为键,并使用链式哈希集合(LinkedHashSet)来存储键的集合。这里使用了链式哈希集合是因为它既可以快速添加和删除元素,又可以保持插入顺序。

LFU缓存的操作主要包括 get 和 put 两个方法。

get(int key) 方法的实现如下:

  1. 首先检查缓存中是否包含该键,如果存在,则需要更新键的使用频率。
  2. 更新使用频率的操作包括将键的使用计数加一,将键从原来使用计数对应的集合中删除,再将键添加到新的使用计数对应的集合中。
  3. 最后返回键对应的值。

put(int key, int value) 方法的实现如下:

  1. 首先检查缓存中是否已经存在该键,如果存在,则更新键对应的值,并更新使用频率。
  2. 如果缓存已满,需要删除一个最不经常使用的键(即使用频率最低的键)。通过查找 frequencyKeys 中对应的最小使用频率,找到对应的键集合,从集合中删除一个键。如果该键集合为空,需要将该使用频率从 frequencyKeys 中删除。
  3. 添加新键值对到缓存中,设置键的使用计数为1,并将键添加到使用计数为1的集合中。

整个实现过程主要依赖于哈希表和链式哈希集合的高效查询和操作,以及通过比较使用频率来确定最不经常使用的键。这种LFU缓存算法能够保持高频率访问的键在缓存中长时间保存,而低频率访问的键则会被逐渐淘汰掉。

这个版本的代码与之前的方法二:TreeMap相比,只是将存储具有相同使用计数的键的集合 frequencyKeys 从 TreeMap 改为了 HashMap,同时使用了 LinkedHashSet 来保持插入顺序。使用 Collections.min 来快速获取最小频率。

实际上,在大多数情况下,由于哈希表的高效性能,这个版本和之前的版本在时间和空间复杂度上没有太大差别。

复杂度分析:

时间复杂度分析:

  • 对于 get 操作,由于只需要在哈希表中查找键值,并且更新键的使用频率,其时间复杂度为 O(1)。
  • 对于 put 操作,如果缓存中已有相同的键,则需要更新该键对应的值,并将键的使用频率加一。这里的时间复杂度也是 O(1)。
  • 如果缓存未满,可以直接将新键值对添加到哈希表中,同时将键的使用频率设置为1。这个操作的时间复杂度也是 O(1)。
  • 当缓存已满时,需要先删除一个最不经常使用的键。这里需要查找使用频率最低的键,然后从集合中删除。由于使用了哈希表和链式哈希集合,查找最小频率的键和删除某个键的时间复杂度均为 O(1)。

因此,整个算法的时间复杂度为 O(1)。

空间复杂度分析:

  • 空间复杂度主要取决于缓存的容量和缓存中存储的键值对数量。由于缓存容量是固定的,所以空间复杂度为 O(capacity)。
  • 注意到使用了三个哈希表来维护键值对、键的使用计数以及具有相同使用计数的键的集合,所以存储键值对和键的使用计数需要额外的空间。但由于哈希表能够快速查找,所以缓存中存储的键值对数量不会很大,因此空间复杂度为 O(capacity)。
  • 另外,由于使用了链式哈希集合来维护具有相同使用计数的键的集合,并且将其按频率从低到高存储在哈希表中,所以这里的空间复杂度也是 O(capacity)。

因此,总的空间复杂度为 O(capacity)。

LeetCode运行结果:

第二题

题目来源

35. 搜索插入位置 - 力扣(LeetCode)

题目内容

解决方法

方法一:二分查找

  1. 在每一次迭代中,通过计算中间元素的索引 mid,并将其与目标值进行比较。根据比较结果,可以确定目标值在数组中的位置。
  2. 如果中间元素等于目标值,则直接返回中间元素的索引。
  3. 如果中间元素小于目标值,则说明目标值应该在右半部分,更新左指针 left 为 mid + 1。
  4. 如果中间元素大于目标值,则说明目标值应该在左半部分,更新右指针 right 为 mid - 1。
  5. 当循环结束时,左指针 left 的位置就是目标值应该插入的位置,因为 left 右侧的数字都大于或等于目标值,而 left 左侧的数字都小于目标值。
class Solution {public int searchInsert(int[] nums, int target) {int left = 0;int right = nums.length - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] == target) {return mid;} else if (nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}// 如果循环结束仍未找到目标值,则返回应插入的位置(即左指针的位置)return left;}
}

复杂度分析:

  • 时间复杂度为 O(log n),其中 n 是数组的长度。每次迭代都将搜索范围缩小一半,因此算法的时间复杂度为对数级别。
  • 空间复杂度为 O(1),因为该算法只使用了常数级别的额外空间。无论输入数组的大小如何,算法所使用的额外空间都是固定的,与数组大小无关。

LeetCode运行结果:

方法二:线性搜索

除了二分查找算法之外,还可以使用线性搜索的方法来解决搜索插入位置的问题。

class Solution {public int searchInsert(int[] nums, int target) {int i = 0;while (i < nums.length && nums[i] < target) {i++;}return i;}
}

在这个方法中,我们使用一个索引变量 i 来遍历数组 nums。在每次迭代中,我们检查当前数字是否小于目标值。如果是,则继续向后移动 i,直到找到第一个大于等于目标值的位置或者达到数组的末尾。

最终,返回的 i 就是目标值应该插入的位置。

复杂度分析:

  • 使用线性搜索方法解决搜索插入位置问题的时间复杂度为 O(n),其中 n 是数组的长度。在最坏情况下,需要遍历整个数组才能确定插入位置或者达到数组的末尾。
  • 空间复杂度为 O(1),因为该方法只使用了常数级别的额外空间,不随输入规模变化。

因此,虽然线性搜索方法不符合题目要求的时间复杂度 O(log n),但仍然可以得到正确的结果。在处理规模较小的数组时,线性搜索方法是可行的选择。如果需要更高效的算法,可以考虑使用二分查找算法。

LeetCode运行结果:

方法三:Arrays类的binarySearch方法

除了二分查找和线性搜索,还有一种方法可以使用Java来解决搜索插入位置问题,即使用Arrays类的binarySearch方法。

import java.util.Arrays;class Solution {public int searchInsert(int[] nums, int target) {int index = Arrays.binarySearch(nums, target);// 如果找到目标值,则直接返回索引if (index >= 0) {return index;} else {// 如果未找到目标值,则返回应插入的位置(取反并减一)return -index - 1;}}
}

在这个方法中,我们使用Arrays类的binarySearch方法来搜索目标值在数组中的位置。如果找到目标值,则直接返回该索引;如果未找到目标值,则返回应插入的位置。

需要注意的是,使用Arrays类的binarySearch方法前,需要确保数组是有序的。如果数组无序,那么需要先对数组进行排序,再使用binarySearch方法。

复杂度分析:

  • 使用Arrays类的binarySearch方法解决搜索插入位置问题的时间复杂度为O(log n),其中n是数组的长度。这是因为binarySearch方法利用二分查找的思想,在每一轮迭代中将搜索范围缩小一半,直到找到目标值或无法再缩小搜索范围为止。
  • 空间复杂度为O(1),因为该方法只使用了常数级别的额外空间。

需要注意的是,使用binarySearch方法前需要确保数组是有序的。如果数组无序,那么需要先对数组进行排序,再使用binarySearch方法。对于未排序数组的排序操作,时间复杂度为O(n log n)。

因此,在确定数组是有序的情况下,使用Arrays类的binarySearch方法可以在较低的时间复杂度内解决搜索插入位置问题。如果数组无序,则需要先进行排序,导致时间复杂度增加。

LeetCode运行结果:

方法四:插入排序

除了二分查找、使用Arrays类的binarySearch方法和线性搜索,还有一种方法是使用插入排序的思想来解决搜索插入位置问题。

class Solution {public int searchInsert(int[] nums, int target) {int i = 0;// 找到第一个大于等于目标值的位置while (i < nums.length && nums[i] < target) {i++;}// 将目标值插入到找到的位置int[] newArr = new int[nums.length + 1];for (int j = 0; j < i; j++) {newArr[j] = nums[j];}newArr[i] = target;for (int j = i; j < nums.length; j++) {newArr[j + 1] = nums[j];}return i;}
}

在这个方法中,我们使用一个索引变量i来找到第一个大于等于目标值的位置。然后,我们创建一个新的数组newArr,将目标值插入到找到的位置,并将原数组中的元素按照顺序复制到新数组中。

最后,返回的i就是目标值应该插入的位置。

复杂度分析:

  • 使用插入排序思想解决搜索插入位置问题的时间复杂度为O(n),其中n是数组的长度。这是因为在最坏情况下,每次都需要遍历整个数组来找到第一个大于等于目标值的位置。
  • 空间复杂度为O(n+1),因为需要创建一个新的长度比原数组多一的数组来存储插入后的结果。

需要注意的是,虽然此方法的时间复杂度较高,但它仍然可以正确地解决搜索插入位置问题。如果对时间复杂度有更高的要求,可以使用二分查找或Arrays类的binarySearch方法。二分查找的时间复杂度为O(log n),Arrays类的binarySearch方法的时间复杂度也为O(log n)。

因此,在处理较大规模的数据时,推荐使用二分查找或Arrays类的binarySearch方法来解决搜索插入位置问题,以获得更好的时间复杂度。而插入排序思想适用于规模较小的问题或者不需要高效解法的情况下。

LeetCode运行结果:

第三题

题目来源

36. 有效的数独 - 力扣(LeetCode)

题目内容

解决方法

方法一:二维数组

class Solution {public boolean isValidSudoku(char[][] board) {// 用三个布尔型数组分别记录每行、每列、每个九宫格中数字是否出现过boolean[][] row = new boolean[9][9];boolean[][] col = new boolean[9][9];boolean[][] block = new boolean[9][9];for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {if (board[i][j] != '.') {int num = board[i][j] - '1';int k = (i / 3) * 3 + j / 3; // 计算当前位置所在的九宫格编号if (row[i][num] || col[j][num] || block[k][num]) { // 如果当前数已经出现过return false;}row[i][num] = true;col[j][num] = true;block[k][num] = true;}}}return true;}
}

该题解法比较简单,使用三个布尔型二维数组来表示数独中每行、每列、每个九宫格中数字是否出现过。对于每个非空数字,根据其所在的行、列、九宫格编号判断该数字是否已经出现过。如果出现过,则返回false,否则标记为出现过。 

复杂度分析:

  • 对于给定的9x9数独,我们遍历了所有的格子,所以时间复杂度为O(1)。
  • 空间复杂度为O(1),因为我们只使用了有限数量的额外空间来存储布尔数组,不随输入规模增长。无论输入的数独尺寸如何变化,额外空间的大小都保持不变。

因此,该算法的时间复杂度和空间复杂度都是常数级别的,具有很高的效率。

LeetCode运行结果:

方法二:哈希集合

除了使用二维数组之外,还可以使用哈希集合来实现判断数独是否有效的算法。

class Solution {public boolean isValidSudoku(char[][] board) {Set<String> seen = new HashSet<>();for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {char digit = board[i][j];if (digit != '.') {// 检查当前数字是否已经出现过if (!seen.add(digit + " in row " + i) ||!seen.add(digit + " in column " + j) ||!seen.add(digit + " in block " + i/3 + "-" + j/3)) {return false;}}}}return true;}
}

这种方法利用了哈希集合的无重复性质。我们遍历数独中的每个格子,对于非空格子,将当前数字加入三个不同的字符串形式的键值,分别是它所在的行、列以及九宫格。如果添加操作失败说明该数字在相应的行、列或九宫格内已经存在,即数独无效。 

复杂度分析:

  • 对于给定的9x9数独,我们遍历了所有的格子,所以时间复杂度为O(1)。
  • 空间复杂度为O(1),因为我们只使用了有限数量的额外空间来存储哈希集合中的元素。无论输入的数独尺寸如何变化,额外空间的大小都保持不变。

因此,该算法的时间复杂度和空间复杂度都是常数级别的,具有很高的效率。与二维数组方法相比,这种基于哈希集合的实现可能更加简洁,但实际性能可能略有差异,具体取决于实际情况和测试数据。

LeetCode运行结果:

方法三:单一遍历

除了使用二维数组和哈希集合之外,还可以通过单一遍历的方式来检查数独的有效性。

class Solution {public boolean isValidSudoku(char[][] board) {int[] rows = new int[9];int[] columns = new int[9];int[] blocks = new int[9];for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {char digit = board[i][j];if (digit != '.') {int mask = 1 << (digit - '1');int blockIndex = (i / 3) * 3 + j / 3;if ((rows[i] & mask) != 0 || (columns[j] & mask) != 0 ||(blocks[blockIndex] & mask) != 0) {return false;}rows[i] |= mask;columns[j] |= mask;blocks[blockIndex] |= mask;}}}return true;}
}

这种方法使用三个一维数组来分别表示每行、每列和每个九宫格中数字是否出现过。对于每个非空格子,使用位运算来标记数字的出现情况。具体地,使用一个32位整数作为位掩码,将位掩码的对应位设置为1表示该数字已经出现过。如果在任何一个数组中发现重复的位掩码,则说明数独无效。 

复杂度分析:

  • 对于给定的9x9数独,我们遍历了所有的格子,所以时间复杂度为O(1)。
  • 空间复杂度为O(1),因为我们只使用了三个固定大小的数组(每个数组大小为9)来存储数字的出现情况。无论输入的数独尺寸如何变化,额外空间的大小都保持不变。

因此,该算法的时间复杂度和空间复杂度都是常数级别的,具有很高的效率。与二维数组和哈希集合方法相比,这种基于单一遍历的实现可能更加简洁,并且减少了额外的存储空间,可能在某些情况下性能略有提升。但实际性能可能略有差异,具体取决于实际情况和测试数据。

LeetCode运行结果:

方法四:位运算

使用位运算来进行判断,利用了三个一维数组`row`、`col`和`area`来保存数独中每行、每列和每个九宫格的数字状态。

算法的实现逻辑如下:

1. 使用两个循环遍历数独的每个格子。
2. 对于非空格子,获取该格子的数字并将其转化为整数。
3. 根据当前格子所在的行、列和九宫格的索引,使用位运算来判断该数字是否在对应的行、列和九宫格中已经出现过。如果出现重复,则返回`false`。
4. 如果数字在当前行、列和九宫格中没有出现过,则更新对应的状态数组。
5. 最后,如果遍历完所有的格子都没有发现重复数字,则返回`true`,表示数独是有效的。

这种方法使用位运算来实现判断,可以减少内存的使用,并且具有较好的性能。

需要注意的是,该方法只能判断数独是否有效,而不能求解数独的解。

class Solution {public boolean isValidSudoku(char[][] board) {int[] row = new int[10], col = new int[10], area = new int[10]; // 分别表示行、列、九宫格的数字状态数组for (int i = 0; i < 9; i++) { // 遍历每个格子for (int j = 0; j < 9; j++) {char c = board[i][j];if (c == '.') continue; // 空白格子跳过int u = c - '0'; // 将字符转换为数字int idx = i / 3 * 3 + j / 3; // 计算九宫格索引// 使用位运算判断数字是否已经在对应的行、列、九宫格中出现过if ((((row[i] >> u) & 1) == 1) || (((col[j] >> u) & 1) == 1) || (((area[idx] >> u) & 1) == 1)) return false;// 更新行、列、九宫格的数字状态row[i] |= (1 << u);col[j] |= (1 << u);area[idx] |= (1 << u);}}return true; // 数独有效}
}

复杂度分析:

时间复杂度分析:

  • 外层循环遍历数独的每一行,内层循环遍历数独的每一列,因此总共有 9 行 * 9 列 = 81 个格子需要遍历。
  • 内部的位运算操作以及数组更新操作都是固定时间的常数操作。
  • 因此,整体的时间复杂度为 O(1),即与输入规模无关。

空间复杂度分析:

  • 空间复杂度由三个长度为 10 的一维数组 rowcol 和 area 决定,这是一个固定大小的数组。
  • 不随输入规模变化,因此空间复杂度也为 O(1),与输入规模无关。

综上所述,这段代码的时间复杂度和空间复杂度均为 O(1)。这是一种高效的解决方案。

LeetCode运行结果:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/87630.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoot的在线文档管理系统

目录 前言 一、技术栈 二、系统功能介绍 管理员功能模块 员工功能模块 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势…

ArcGIS 10.3软件安装包下载及安装教程!

【软件名称】&#xff1a;ArcGIS 10.3 【安装环境】&#xff1a;Windows 【下载链接 】&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1K5ab7IHMYa23HpmuPkFa1A 提取码&#xff1a;oxbb 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 软件解压码点击原文…

java面试题-jvm基础知识

1 JVM组成 1.1 JVM由那些部分组成&#xff0c;运行流程是什么&#xff1f; 难易程度&#xff1a;☆☆☆ 出现频率&#xff1a;☆☆☆☆ JVM是什么 Java Virtual Machine Java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09; 好处&#xff1a; 一次编写&a…

代码随想录算法训练营day6| 哈希表理论基础、242.有效的字母异位词、349. 两个数组的交集、202. 快乐数、1. 两数之和

目录 一、哈希表理论 hash function ​编辑hash collision 常见哈希结构 1&#xff09;set 2&#xff09;map 二、&#xff08;leetcode 242&#xff09;有效的字母异位词 三、&#xff08;leetcode 349&#xff09;两个数组的交集 四、&#xff08;leetcode 202&…

JAVA学习-全网最详细

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…

Linux学习第19天:Linux并发与竞争实例: 没有规矩不成方圆

Linux版本号4.1.15 芯片I.MX6ULL 大叔学Linux 品人间百味 思文短情长 先说点题外话&#xff0c;上周参加行业年会&#xff0c;停更了一周。接下来的周五就要开启国庆中秋双节模式&#xff0c;所以有的时候&#xff0c;尤其是工作以后…

java面试题-设计模式基础

面试专题-设计模式 前言 在平时的开发中&#xff0c;涉及到设计模式的有两块内容&#xff0c;第一个是我们平时使用的框架&#xff08;比如spring、mybatis等&#xff09;&#xff0c;第二个是我们自己开发业务使用的设计模式。 面试官一般比较关心的是你在开发过程中&#…

游戏录屏软件推荐,教你录制高清游戏视频

“有没有好用的游戏录屏软件推荐呀&#xff0c;最近当上了游戏主播&#xff0c;平台要求每天都要发一个游戏视频&#xff0c;可是我的游戏录屏软件太拉胯了&#xff0c;录制出来的视频非常糊&#xff0c;导致平台审核不通过&#xff0c;所以想问问大家有没有游戏录屏软件推荐一…

SQL server 创建存储过程

SQL Server如何创建存储过程 存储过程&#xff1a; 可以理解为完成特定功能的一组 SQL 语句集&#xff0c;存储在数据库中&#xff0c;经过第一次编译&#xff0c;之后的运行不需要再次编译&#xff0c;用户通过指定存储过程的名字并给出参数&#xff08;如果该存储过程带有参数…

时序分解 | Matlab实现CEEMD互补集合经验模态分解时间序列信号分解

时序分解 | Matlab实现CEEMD互补集合经验模态分解时间序列信号分解 目录 时序分解 | Matlab实现CEEMD互补集合经验模态分解时间序列信号分解效果一览基本介绍程序设计参考资料 效果一览 基本介绍 Matlab实现CEEMD互补集合经验模态分解时间序列信号分解 1.分解效果图 &#xff0…

opencv dnn模块 示例(17) 目标检测 object_detection 之 yolo v5

在前文【opencv dnn模块 示例(16) 目标检测 object_detection 之 yolov4】介绍的yolo v4后的2个月&#xff0c;Ultralytics发布了YOLOV5 的第一个正式版本&#xff0c;其性能与YOLO V4不相伯仲。 文章目录 1、Yolo v5 和 Yolo v4 的区别说明1.1、Data Augmentation - 数据增强1…

vue3【echarts 做的词云图】

效果图 安装 安装echarts npm install echarts安装词云图 npm install echarts-wordcloudecharts-wordcloud的git仓库地址 echarts官网地址 引用 import * as ECharts from "echarts"; //引用eacharts import echarts-wordcloud;//引用云词这里的echarts 是自己简…

PHP8的类与对象的基本操作之类的实例化-PHP8知识详解

定义完类和方法后&#xff0c;并不是真正创建一个对象。类和对象可以描述为如下关系。类用来描述具有相同数据结构和特征的“一组对象”&#xff0c;“类”是“对象”的抽象&#xff0c;而“对象”是“类”的具体实例&#xff0c;即一个类中的对象具有相同的“型”&#xff0c;…

【Solidity】Solidity Keccak256 与 SHA3-256

Solidity Keccak256 与 SHA3-256 文章目录 前言什么是 SHA3Keccak256 和 SHA3-256 有什么区别?为何推出sha3参考区块链的造富神话大家一定都有所耳闻,今天我们讨论以太坊中一项基础技术,主打一个一学就会。 前言 看过以太坊源码或者对区块链有了解的的同学,一定都见过一个…

华为 Mate60 系列全球发布:地表最强黑科技旗舰,打破传统,引领未来!

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

Amazon Lightsail——兼具亚马逊云科技的强大功能与 VPS 的简易性

对于开发者而言&#xff0c;当你想构建系统架构时&#xff0c;你的面前就出现了两种选择&#xff0c;选择一&#xff1a;花时间去亲手挑选每个亚马逊云科技组件&#xff08;云服务器、存储、IP 地址等&#xff09;&#xff0c;然后自己组装起来&#xff1b;选择二是只需要一个预…

WebGL HUD(平视显示器)

目录 HUD&#xff08;平视显示器&#xff09; 如何实现HUD 示例程序&#xff08;HUD.html&#xff09; 示例程序&#xff08;HUD.js&#xff09; 代码详解 在网页文字上方显示三维物体 代码详解 HUD&#xff08;平视显示器&#xff09; 平视显示器&#xff08;head…

展示日志log4.properties

log4.properties 1.log4.properties 此时文件主要用于展示日志的输出的级别的信息。 # Set root category priority to INFO and its only appender to CONSOLE. #log4j.rootCategoryINFO, CONSOLE debug info warn error fatal log4j.rootCategoryinfo, CONSO…

算法通过村第九关-二分(中序遍历)黄金笔记|二叉搜索树

文章目录 前言1. 有序数组转二叉搜索树2. 寻找连个正序数组的中位数总结 前言 提示&#xff1a;有时候&#xff0c;我感觉自己一辈子活在两个闹钟之间&#xff0c;早上的第一次闹钟&#xff0c;以及5分钟之后的第二次闹钟。 --奥利弗萨克斯《意识的河流》 每个专题都有简单题&a…

netty快速入门基本用法

1、netty的基本工作流程 在netty中存在以下的核心组件&#xff1a; ServerBootstrap&#xff1a;服务器端启动辅助对象&#xff1b;Bootstrap&#xff1a;客户端启动辅助对象&#xff1b;Channel&#xff1a;通道&#xff0c;代表一个连接&#xff0c;每个Client请对会对应到…