怒刷LeetCode的第15天(Java版)

news2024/11/18 12:40:05

目录

第一题

题目来源

题目内容

解决方法

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

方法二: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.coloradmin.cn/o/1039806.html

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

相关文章

JAVA学习-全网最详细

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

1960-2017年世界各国总和生育率数据

1960-2017年世界各国总和生育率数据 1、时间&#xff1a;1960-2017年 2、指标&#xff1a;生育率 3、范围&#xff1a;全球各国 4、来源&#xff1a;世界银行 5、指标解释&#xff1a; 总生育率表示假设妇女度过整个生育期并按照当期的年龄别生育率生育孩子所生育的孩子数…

第一次课进行分类代码

System32下的进程 #include <windows.h> #include <stdio.h> #include<TlHelp32.h> #include<psapi.h>int main() {HANDLE hProcessSnap;PROCESSENTRY32 pe32;// 获取进程快照hProcessSnap CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);if (hPr…

微信编辑器自带导出功能,同步到公众号生成链接

​在微信编辑器里编辑好了文章&#xff0c;想把编辑器里的文章复制到其它网站或者分享给好友&#xff0c;怎么操作呢&#xff1f;其实很简单&#xff0c;可以通过编辑器自带的导出功能&#xff0c;或者同步到微信公众号生成链接&#xff0c;那今天小编先给大家说一说编辑器自带…

python setup.py egg_info“ failed with error code 1 in xxxxxxxx问题解决

python setup.py egg_info" failed with error code 1 in xxxxxxxx问题解决 一、问题描述&#xff1a;通过pip安装opencv-python时候&#xff0c;提示安装二、解决办法&#xff1a;升级pip三、结果 一、问题描述&#xff1a;通过pip安装opencv-python时候&#xff0c;提示…

Python 机器学习入门之线性回归

系列文章目录 第一章 Python 机器学习入门之线性回归 线性回归 系列文章目录前言一、线性回归1.线性回归是什么2.线性回归的分类 二、实现线性回归1.步骤2.代价函数3.梯度下降 总结 前言 最近在上机器学习的课程&#xff0c;第一次实验是做线性回归&#xff0c;那神马是线性回…

基于FPGA的图像坏点像素修复算法实现,包括tb测试文件和MATLAB辅助验证

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 vivado2019.2 matlab2022a 3.部分核心程序 timescale 1ns / 1ps // // Company: // Engineer: // // Create Date: 202…

竞赛选题 基于生成对抗网络的照片上色动态算法设计与实现 - 深度学习 opencv python

文章目录 1 前言1 课题背景2 GAN(生成对抗网络)2.1 简介2.2 基本原理 3 DeOldify 框架4 First Order Motion Model5 最后 1 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于生成对抗网络的照片上色动态算法设计与实现 该项目较为新颖&am…

Linux 读写锁

读写锁是一把锁 /*读写锁的类型 pthread_rwlock_tpthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);int pthread_rwlock_destory(pthread_rwlock_t *rwlock);int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);int pt…

Android逆向技术高阶大法

原文链接 Android逆向技术高阶大法 安卓应用是一个客户端&#xff0c;与传统软件类似&#xff0c;需要把软件打包&#xff0c;然后通过某种渠道&#xff08;应用市场&#xff09;分发给用户&#xff0c;这是常规的发布方式&#xff0c;它的更新节奏很慢&#xff0c;从你在应用…

系统集成|第十四章(笔记)

目录 第十四章 合同管理14.1 概述及相关概念14.2 项目合同14.3 《合同法》14.4 《仲裁法》 上篇&#xff1a;第十三章、干系人管理 第十四章 合同管理 14.1 概述及相关概念 主要包括合同签订管理&#xff0c;合同履行管理&#xff0c;合同变更管理以及合同档案管理。作为一个重…

自拟实现消息队列(MQ)基于Rabbit MQ(含概念和源码)巨详细!!!!!含思维导图

MQ目录 MQ基本概念什么是MQ&#xff1f;MQ的应用场景 首先先明白需求持久化分析那么MQ如何设计持久化&#xff1f; 可靠性分析高效性分析MQ核心概念&#xff08;装配层&#xff09;实现MQ组件思维导图创建项目导入数据库下载SqLite。 创建组件实体类创建交换机&#xff08;要加…

精通Linux系列第一章:探索Linux世界的大门

文章目录 一、前言二、 什么是Linux&#xff1f;三、Linux系统与Windows系统的区别四、为什么要学习Linux&#xff1f;五、 Linux的优势六、什么是Linux发行版&#xff1f;七、常见的Linux发行版八、如何选择适合你的Linux发行版&#xff1f;九、Linux各种发行版的优势与应用十…

【C语言】进阶——结构体+枚举+联合

①前言&#xff1a; 在之前【C语言】初阶——结构体 &#xff0c;简单介绍了结构体。而C语言中结构体的内容还有更深层次的内容。 一.结构体 结构体(struct)是由一系列具有相同类型或不同类型的数据项构成的数据集合&#xff0c;这些数据项称为结构体的成员。 1.结构体的声明 …

NVM:node多版本管理的下载安装及使用

NVM&#xff1a;node多版本管理的下载安装及使用 使用之前先卸载node&#xff0c;避免各种奇葩问题导致不成功。win卸载&#xff1a;win > 设置 > 应用 > 应用和功能&#xff0c;找到 node 点击出现卸载按钮并且卸载它。 1、下载安装&#xff1a; https://github.co…

信创之国产浪潮电脑+统信UOS操作系统体验1:硬件及软件常规功能支持情况介绍

一、引言 由于公司要求支持国产信创&#xff0c;最近办公的笔记本电脑换成了软硬件全国产&#xff0c;由于国产操作系统是在开源linux基础上演进的&#xff0c;在换之前&#xff0c;非常担心操作不方便&#xff0c;周边应用软件少&#xff0c;功能差&#xff0c;内心是比较抗拒…

C++:优先级队列模拟实现和仿函数的概念使用

文章目录 使用方法Compare仿函数一些场景模板参数和函数参数 本篇总结优先级队列 使用方法 首先在官网查看它的一些用法 template <class T, class Container vector<T>,class Compare less<typename Container::value_type> > class priority_queue;从…

软件测试之接口测试

1、什么是接口测试 顾名思义&#xff0c;接口测试是对系统或组件之间的接口进行测试&#xff0c;主要是校验数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及相互逻辑依赖关系。其中接口协议分为HTTP,WebService,Dubbo,Thrift,Socket等类型&#xff0c;测试类型又主…

Crypto:MD5

题目 下载了题目给的压缩包解压后&#xff0c;打开文件 使用md5解码器解码后得到&#xff0c;即为flag

DS18B20温度传感器

DS18B20简介 DS18B20 是由 DALLAS 半导体公司推出的一种的“一线总线&#xff08;单总线&#xff09;”接口的温度传感器 这种一线总线就是 三线制 SPI DS18B20的 配置寄存器&#xff1a; TM 是测试位&#xff0c;出厂设置就被设置为0&#xff0c;不需要改动&#xff0c; R1、R…