阅读视图

发现新文章,点击刷新页面。

[Python3/Java/C++/Go/TypeScript] 一题一解:哈希表(清晰题解)

方法一:哈希表

我们用一个哈希表 $\textit{d}$ 来记录所有单元格的值,其中键为单元格引用,值为单元格的值。

调用 setCell 方法时,我们将单元格引用和值存入哈希表中。

调用 resetCell 方法时,我们将单元格引用从哈希表中删除。

调用 getValue 方法时,我们将公式分割成两个单元格引用,然后计算它们的值,最后返回它们的和。

###python

class Spreadsheet:

    def __init__(self, rows: int):
        self.d = {}

    def setCell(self, cell: str, value: int) -> None:
        self.d[cell] = value

    def resetCell(self, cell: str) -> None:
        self.d.pop(cell, None)

    def getValue(self, formula: str) -> int:
        ans = 0
        for cell in formula[1:].split("+"):
            ans += int(cell) if cell[0].isdigit() else self.d.get(cell, 0)
        return ans


# Your Spreadsheet object will be instantiated and called as such:
# obj = Spreadsheet(rows)
# obj.setCell(cell,value)
# obj.resetCell(cell)
# param_3 = obj.getValue(formula)

###java

class Spreadsheet {
    private Map<String, Integer> d = new HashMap<>();

    public Spreadsheet(int rows) {
    }

    public void setCell(String cell, int value) {
        d.put(cell, value);
    }

    public void resetCell(String cell) {
        d.remove(cell);
    }

    public int getValue(String formula) {
        int ans = 0;
        for (String cell : formula.substring(1).split("\\+")) {
            ans += Character.isDigit(cell.charAt(0)) ? Integer.parseInt(cell)
                                                     : d.getOrDefault(cell, 0);
        }
        return ans;
    }
}

/**
 * Your Spreadsheet object will be instantiated and called as such:
 * Spreadsheet obj = new Spreadsheet(rows);
 * obj.setCell(cell,value);
 * obj.resetCell(cell);
 * int param_3 = obj.getValue(formula);
 */

###cpp

class Spreadsheet {
private:
    unordered_map<string, int> d;

public:
    Spreadsheet(int rows) {}

    void setCell(string cell, int value) {
        d[cell] = value;
    }

    void resetCell(string cell) {
        d.erase(cell);
    }

    int getValue(string formula) {
        int ans = 0;
        stringstream ss(formula.substr(1));
        string cell;
        while (getline(ss, cell, '+')) {
            if (isdigit(cell[0])) {
                ans += stoi(cell);
            } else {
                ans += d.count(cell) ? d[cell] : 0;
            }
        }
        return ans;
    }
};

/**
 * Your Spreadsheet object will be instantiated and called as such:
 * Spreadsheet* obj = new Spreadsheet(rows);
 * obj->setCell(cell,value);
 * obj->resetCell(cell);
 * int param_3 = obj->getValue(formula);
 */

###go

type Spreadsheet struct {
    d map[string]int
}

func Constructor(rows int) Spreadsheet {
    return Spreadsheet{d: make(map[string]int)}
}

func (this *Spreadsheet) SetCell(cell string, value int) {
    this.d[cell] = value
}

func (this *Spreadsheet) ResetCell(cell string) {
    delete(this.d, cell)
}

func (this *Spreadsheet) GetValue(formula string) int {
    ans := 0
    cells := strings.Split(formula[1:], "+")
    for _, cell := range cells {
        if val, err := strconv.Atoi(cell); err == nil {
            ans += val
        } else {
            ans += this.d[cell]
        }
    }
    return ans
}


/**
 * Your Spreadsheet object will be instantiated and called as such:
 * obj := Constructor(rows);
 * obj.SetCell(cell,value);
 * obj.ResetCell(cell);
 * param_3 := obj.GetValue(formula);
 */

###ts

class Spreadsheet {
    private d: Map<string, number>;

    constructor(rows: number) {
        this.d = new Map<string, number>();
    }

    setCell(cell: string, value: number): void {
        this.d.set(cell, value);
    }

    resetCell(cell: string): void {
        this.d.delete(cell);
    }

    getValue(formula: string): number {
        let ans = 0;
        const cells = formula.slice(1).split('+');
        for (const cell of cells) {
            ans += isNaN(Number(cell)) ? this.d.get(cell) || 0 : Number(cell);
        }
        return ans;
    }
}

/**
 * Your Spreadsheet object will be instantiated and called as such:
 * var obj = new Spreadsheet(rows)
 * obj.setCell(cell,value)
 * obj.resetCell(cell)
 * var param_3 = obj.getValue(formula)
 */

###rust

use std::collections::HashMap;

struct Spreadsheet {
    d: HashMap<String, i32>,
}

impl Spreadsheet {
    fn new(_rows: i32) -> Self {
        Spreadsheet {
            d: HashMap::new(),
        }
    }

    fn set_cell(&mut self, cell: String, value: i32) {
        self.d.insert(cell, value);
    }

    fn reset_cell(&mut self, cell: String) {
        self.d.remove(&cell);
    }

    fn get_value(&self, formula: String) -> i32 {
        let mut ans = 0;
        for cell in formula[1..].split('+') {
            if cell.chars().next().unwrap().is_ascii_digit() {
                ans += cell.parse::<i32>().unwrap();
            } else {
                ans += *self.d.get(cell).unwrap_or(&0);
            }
        }
        ans
    }
}

时间复杂度 $(L)$,空间复杂度 $(L)$。其中 $L$ 为公式的长度。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-设计电子表格🟡

电子表格是一个网格,它有 26 列(从 'A''Z')和指定数量的 rows。每个单元格可以存储一个 0 到 105 之间的整数值。

请你实现一个 Spreadsheet 类:

  • Spreadsheet(int rows) 初始化一个具有 26 列(从 'A''Z')和指定行数的电子表格。所有单元格最初的值都为 0 。
  • void setCell(String cell, int value) 设置指定单元格的值。单元格引用以 "AX" 的格式提供(例如,"A1""B10"),其中字母表示列(从 'A''Z'),数字表示从 1 开始的行号。
  • void resetCell(String cell) 重置指定单元格的值为 0 。
  • int getValue(String formula) 计算一个公式的值,格式为 "=X+Y",其中 XY 要么 是单元格引用,要么非负整数,返回计算的和。

注意: 如果 getValue 引用一个未通过 setCell 明确设置的单元格,则该单元格的值默认为 0 。

 

示例 1:

输入:
["Spreadsheet", "getValue", "setCell", "getValue", "setCell", "getValue", "resetCell", "getValue"]
[[3], ["=5+7"], ["A1", 10], ["=A1+6"], ["B2", 15], ["=A1+B2"], ["A1"], ["=A1+B2"]]

输出:
[null, 12, null, 16, null, 25, null, 15]

解释

Spreadsheet spreadsheet = new Spreadsheet(3); // 初始化一个具有 3 行和 26 列的电子表格
spreadsheet.getValue("=5+7"); // 返回 12 (5+7)
spreadsheet.setCell("A1", 10); // 设置 A1 为 10
spreadsheet.getValue("=A1+6"); // 返回 16 (10+6)
spreadsheet.setCell("B2", 15); // 设置 B2 为 15
spreadsheet.getValue("=A1+B2"); // 返回 25 (10+15)
spreadsheet.resetCell("A1"); // 重置 A1 为 0
spreadsheet.getValue("=A1+B2"); // 返回 15 (0+15)

 

提示:

  • 1 <= rows <= 103
  • 0 <= value <= 105
  • 公式保证采用 "=X+Y" 格式,其中 XY 要么是有效的单元格引用,要么是小于等于 105非负 整数。
  • 每个单元格引用由一个大写字母 'A''Z' 和一个介于 1rows 之间的行号组成。
  • 总共 最多会对 setCellresetCellgetValue 调用 104 次。

设计电子表格

方法一:模拟

思路与算法

根据题意可知,我们可以直接使用行数为 $\textit{rows}$,列数为 $26$ 的二维数组 $\textit{grid}$ 来保存每个单元格的值,此时查询和更新单元格时,直接查询和更新二维数组中的元素即可,$\text{Spreadsheet}$ 类实现如下:

  • 初始化:调用 $\text{Spreadsheet(int rows)}$,创建行数为 $\textit{rows}$,列数为 $26$ 的二维数组 $\textit{grid}$,并将数组元素初始化为 $0$;

  • $\text{void setCell(String cell, int value)}$:设置指定单元格的值为 $\textit{value}$,按照规则从 字符串 $\textit{cell}$ 中解析出单元格对应的行数与列数,并将对应的二维数组 $\textit{grid}$ 对应元素的值设置为 $\textit{value}$;

  • $\text{void resetCell(String cell)}$:重置指定单元格的值为 $0$。按照规则从字符串 $\textit{cell}$ 中解析出单元格对应的行数与列数,并将对应的二维数组 $\textit{grid}$ 对应元素的值设置为 $0$;

  • $\text{int getValue(String formula)}$:计算一个公式的值。根据题目为可知公式格式为:"=X+Y",从 $\textit{formula}$ 按照给定格式解析出 $\text{X}$ 与 $\text{Y}$。如果 $\text{X}$ 或 $\text{Y}$ 的首字母为字母,则其为单元格,此时从二维数组中 $\textit{grid}$ 读取对应的值;如果其首字母为数字,则其为整数,分别求出 $\text{X}$ 与 $\text{Y}$ 对应的值,并返回二者相加的和即为答案。

代码

###C++

class Spreadsheet {
public:
    Spreadsheet(int rows) {
        this->grid = vector<vector<int>>(rows + 1, vector<int>(26));
    }

    void setCell(string cell, int value) {
        auto [x, y] = getPos(cell);
        grid[x][y] = value;
    }
    
    void resetCell(string cell) {
        auto [x, y] = getPos(cell);
        grid[x][y] = 0;
    }
    
    int getValue(string formula) {
        int i = formula.find('+');
        string cell1 = formula.substr(1, i - 1);
        string cell2 = formula.substr(i + 1);
        return getCellVal(cell1) + getCellVal(cell2);
    }

private:
    vector<vector<int>> grid;

    pair<int, int> getPos(const string &cell) {
        int x = stoi(cell.substr(1));
        int y = cell[0] - 'A';
        return {x, y};
    }

    int getCellVal(string &cell) {
        if (isalpha(cell[0])) {
            auto [x, y] = getPos(cell);
            return grid[x][y];
        } else {
            return stoi(cell);
        }
    }
};

###Java

public class Spreadsheet {
    private int[][] grid;

    public Spreadsheet(int rows) {
        this.grid = new int[rows + 1][26];
    }

    public void setCell(String cell, int value) {
        int[] pos = getPos(cell);
        grid[pos[0]][pos[1]] = value;
    }
    
    public void resetCell(String cell) {
        int[] pos = getPos(cell);
        grid[pos[0]][pos[1]] = 0;
    }
    
    public int getValue(String formula) {
        int i = formula.indexOf('+');
        String cell1 = formula.substring(1, i);
        String cell2 = formula.substring(i + 1);
        return getCellVal(cell1) + getCellVal(cell2);
    }

    private int[] getPos(String cell) {
        int x = Integer.parseInt(cell.substring(1));
        int y = cell.charAt(0) - 'A';
        return new int[]{x, y};
    }

    private int getCellVal(String cell) {
        if (Character.isLetter(cell.charAt(0))) {
            int[] pos = getPos(cell);
            return grid[pos[0]][pos[1]];
        } else {
            return Integer.parseInt(cell);
        }
    }
}

###C#

public class Spreadsheet {
    private int[,] grid;

    public Spreadsheet(int rows) {
        this.grid = new int[rows + 1, 26];
    }

    public void SetCell(string cell, int value) {
        var pos = GetPos(cell);
        grid[pos.Item1, pos.Item2] = value;
    }
    
    public void ResetCell(string cell) {
        var pos = GetPos(cell);
        grid[pos.Item1, pos.Item2] = 0;
    }
    
    public int GetValue(string formula) {
        int i = formula.IndexOf('+');
        string cell1 = formula.Substring(1, i - 1);
        string cell2 = formula.Substring(i + 1);
        return GetCellVal(cell1) + GetCellVal(cell2);
    }

    private Tuple<int, int> GetPos(string cell) {
        int x = int.Parse(cell.Substring(1));
        int y = cell[0] - 'A';
        return Tuple.Create(x, y);
    }

    private int GetCellVal(string cell) {
        if (char.IsLetter(cell[0])) {
            var pos = GetPos(cell);
            return grid[pos.Item1, pos.Item2];
        } else {
            return int.Parse(cell);
        }
    }
}

###Python

class Spreadsheet:

    def __init__(self, rows: int):
        self.grid = [[0] * 27 for _ in range(rows + 1)]

    def setCell(self, cell: str, value: int) -> None:
        x, y = self.getPos(cell)
        self.grid[x][y] = value

    def resetCell(self, cell: str) -> None:
        x, y = self.getPos(cell)
        self.grid[x][y] = 0

    def getValue(self, formula: str) -> int:
        i = formula.find('+')
        cell1 = formula[1:i]
        cell2 = formula[i + 1:]
        return self.getCellVal(cell1) + self.getCellVal(cell2)

    def getPos(self, cell):
        x = int(cell[1:])
        y = ord(cell[0]) - ord('A')
        return (x, y)

    def getCellVal(self, cell):
        if cell[0].isalpha():
            x, y = self.getPos(cell)
            return self.grid[x][y]
        else:
            return int(cell)

###Go

type Spreadsheet struct {
grid [][]int
}

func Constructor(rows int) Spreadsheet {
    grid := make([][]int, rows+1)
for i := range grid {
grid[i] = make([]int, 27)
}
return Spreadsheet{grid: grid}
}

func (this *Spreadsheet) SetCell(cell string, value int)  {
    x, y := this.getPos(cell)
this.grid[x][y] = value
}

func (this *Spreadsheet) ResetCell(cell string)  {
    x, y := this.getPos(cell)
this.grid[x][y] = 0
}

func (this *Spreadsheet) GetValue(formula string) int {
    i := strings.Index(formula, "+")
cell1 := formula[1:i]
cell2 := formula[i+1:]
return this.getCellVal(cell1) + this.getCellVal(cell2)
}

func (this *Spreadsheet) getPos(cell string) (int, int) {
x, _ := strconv.Atoi(cell[1:])
y := int(cell[0] - 'A')
return x, y
}

func (this *Spreadsheet) getCellVal(cell string) int {
if cell[0] >= 'A' && cell[0] <= 'Z' {
x, y := this.getPos(cell)
return this.grid[x][y]
} else {
val, _ := strconv.Atoi(cell)
return val
}
}

###C

#define COLUMNS 26

typedef struct {
    int** grid;
    int rows;
} Spreadsheet;

void getPos(const char* cell, int* x, int* y) {
    *x = atoi(cell + 1);
    *y = toupper(cell[0]) - 'A';
}

Spreadsheet* spreadsheetCreate(int rows) {
    Spreadsheet* obj = (Spreadsheet*)malloc(sizeof(Spreadsheet));
    obj->rows = rows + 1;
    obj->grid = (int**)malloc(obj->rows * sizeof(int*));
    for (int i = 0; i < obj->rows; i++) {
        obj->grid[i] = (int*)calloc(COLUMNS, sizeof(int));
    }
    return obj;
}

void spreadsheetSetCell(Spreadsheet* obj, char* cell, int value) {
    int x, y;
    getPos(cell, &x, &y);
    obj->grid[x][y] = value;
}

void spreadsheetResetCell(Spreadsheet* obj, char* cell) {
    int x, y;
    getPos(cell, &x, &y);
    obj->grid[x][y] = 0;
}

int getCellVal(Spreadsheet* obj, const char* cell) {
    if (isalpha(cell[0])) {
        int x, y;
        getPos(cell, &x, &y);
        return obj->grid[x][y];
    } else {
        return atoi(cell);
    }
}

int spreadsheetGetValue(Spreadsheet* obj, char* formula) {
    char* plus = strchr(formula, '+');
    char* cell1 = (char*)malloc(plus - formula);
    strncpy(cell1, formula + 1, plus - formula - 1);
    cell1[plus - formula - 1] = '\0';
    char* cell2 = strdup(plus + 1);
    int val1 = getCellVal(obj, cell1);
    int val2 = getCellVal(obj, cell2);
    free(cell1);
    free(cell2);
    return val1 + val2;
}

void spreadsheetFree(Spreadsheet* obj) {
    for (int i = 0; i < obj->rows; i++) {
        free(obj->grid[i]);
    }
    free(obj->grid);
    free(obj);
}

###JavaScript

var Spreadsheet = function(rows) {
    this.grid = Array.from({length: rows + 1}, () => new Array(27).fill(0));
};

Spreadsheet.prototype.setCell = function(cell, value) {
    const [x, y] = this.getPos(cell);
    this.grid[x][y] = value;
};

Spreadsheet.prototype.resetCell = function(cell) {
    const [x, y] = this.getPos(cell);
    this.grid[x][y] = 0;
};

Spreadsheet.prototype.getValue = function(formula) {
    const i = formula.indexOf('+');
    const cell1 = formula.substring(1, i);
    const cell2 = formula.substring(i + 1);
    return this.getCellVal(cell1) + this.getCellVal(cell2);
};

Spreadsheet.prototype.getPos = function(cell) {
    const x = parseInt(cell.substring(1));
    const y = cell.charCodeAt(0) - 'A'.charCodeAt(0);
    return [x, y];
}

Spreadsheet.prototype.getCellVal = function(cell) {
    if (/[A-Z]/.test(cell[0])) {
        const [x, y] = this.getPos(cell);
        return this.grid[x][y];
    } else {
        return parseInt(cell);
    }
}

###TypeScript

class Spreadsheet {
    private grid: number[][];

    constructor(rows: number) {
        this.grid = Array.from({length: rows + 1}, () => new Array(27).fill(0));
    }

    setCell(cell: string, value: number): void {
        const [x, y] = this.getPos(cell);
        this.grid[x][y] = value;
    }

    resetCell(cell: string): void {
        const [x, y] = this.getPos(cell);
        this.grid[x][y] = 0;
    }

    getValue(formula: string): number {
        const i = formula.indexOf('+');
        const cell1 = formula.substring(1, i);
        const cell2 = formula.substring(i + 1);
        return this.getCellVal(cell1) + this.getCellVal(cell2);
    }

    private getPos(cell: string): [number, number] {
        const x = parseInt(cell.substring(1));
        const y = cell.charCodeAt(0) - 'A'.charCodeAt(0);
        return [x, y];
    }

    private getCellVal(cell: string): number {
        if (/[A-Z]/.test(cell[0])) {
            const [x, y] = this.getPos(cell);
            return this.grid[x][y];
        } else {
            return parseInt(cell);
        }
    }
}

###Rust

pub struct Spreadsheet {
    grid: Vec<Vec<i32>>,
}

impl Spreadsheet {
    pub fn new(rows: i32) -> Self {
        Spreadsheet {
            grid: vec![vec![0; 27]; (rows + 1) as usize],
        }
    }
    
    pub fn set_cell(&mut self, cell: String, value: i32) {
        let (x, y) = self.get_pos(&cell);
        self.grid[x][y] = value;
    }
    
    pub fn reset_cell(&mut self, cell: String) {
        let (x, y) = self.get_pos(&cell);
        self.grid[x][y] = 0;
    }
    
    pub fn get_value(&self, formula: String) -> i32 {
        let i = formula.find('+').unwrap();
        let cell1 = &formula[1..i];
        let cell2 = &formula[i+1..];
        self.get_cell_val(cell1) + self.get_cell_val(cell2)
    }

    fn get_pos(&self, cell: &str) -> (usize, usize) {
        let x = cell[1..].parse::<usize>().unwrap();
        let y = cell.chars().next().unwrap() as usize - 'A' as usize;
        (x, y)
    }

    fn get_cell_val(&self, cell: &str) -> i32 {
        if cell.chars().next().unwrap().is_ascii_alphabetic() {
            let (x, y) = self.get_pos(cell);
            self.grid[x][y]
        } else {
            cell.parse().unwrap()
        }
    }
}

复杂度分析

  • 时间复杂度:初始化表格时对应的时间复杂度为 $O(C \times \textit{rows})$,其余操作对应的时间复杂度为 $O(1)$,其中 $\textit{rows}$ 表示给定的电子表格的行数,$C$ 表示给定的二维表格的列数,在本题中 $C = 26$。初始化时需要创建一个行数为 $\textit{rows}$,列数为 $26$ 的二维数组,此时需要的时间为 $O(C \times \textit{rows})$,其余操作仅涉及到解析字符串,并定位到单元格需要的时间为 $O(1)$。

  • 空间复杂度:$O(C \times \textit{rows})$,其中 $\textit{rows}$ 表示给定的电子表格的行数,$C$ 表示给定的二维表格的列数,在本题中 $C = 26$。创建一个行数为 $\textit{rows}$,列数为 $26$ 的二维数组,需要的空间为 $O(C \times \textit{rows})$。

方法二:哈希表

思路与算法

根据题意可知,由于每个单元格的标识均不同,我们直接使用哈希表来保存单元格的值,此时更新和充值单元格时,直接更新哈希表即可,$\text{Spreadsheet}$ 类实现如下:

  • 初始化:调用 $\text{Spreadsheet(int rows)}$:初始化单元格。此时直接初始化哈希表 $\textit{cellValues}$。

  • $\text{void setCell(String cell, int value)}$:设置指定单元格的值为 $\textit{value}$,将哈希表中索引 $\textit{cell}$ 对应的元素值设置为 $\textit{value}$;

  • $\text{void resetCell(String cell)}$:重置指定单元格的值为 $0$。从哈希表中删除索引为 $\textit{cell}$ 的元素;

  • $\text{int getValue(String formula)}$:计算一个公式的值。根据题目为可知公式格式为:"=X+Y",从 $\textit{formula}$ 按照给定格式解析出 $\text{X}$ 与 $\text{Y}$。如果 $\text{X}$ 或 $\text{Y}$ 的首字母为字母,则其为单元格,此时从哈希表 $\textit{cellValues}$ 读取对应的值;如果其首字母为数字则将其转换为整数,分别求出 $\text{X}$ 与 $\text{Y}$ 对应的值,并返回二者相加的和即为答案。

代码

###C++

class Spreadsheet {
public:
    Spreadsheet(int) {}

    void setCell(string cell, int value) {
        cellValues[cell] = value;
    }

    void resetCell(string cell) {
        cellValues.erase(cell);
    }

    int getValue(string formula) {
        int i = formula.find('+');
        string cell1 = formula.substr(1, i - 1);
        string cell2 = formula.substr(i + 1);
        return (isalpha(cell1[0]) ? cellValues[cell1] : stoi(cell1)) +
               (isalpha(cell2[0]) ? cellValues[cell2] : stoi(cell2));
    }

private:
    unordered_map<string, int> cellValues;
};

###Java

public class Spreadsheet {
    private Map<String, Integer> cellValues = new HashMap<>();

    public Spreadsheet(int size) {

    }

    public void setCell(String cell, int value) {
        cellValues.put(cell, value);
    }

    public void resetCell(String cell) {
        cellValues.remove(cell);
    }

    public int getValue(String formula) {
        int i = formula.indexOf('+');
        String cell1 = formula.substring(1, i);
        String cell2 = formula.substring(i + 1);
        int val1 = Character.isLetter(cell1.charAt(0)) ? cellValues.getOrDefault(cell1, 0) : Integer.parseInt(cell1);
        int val2 = Character.isLetter(cell2.charAt(0)) ? cellValues.getOrDefault(cell2, 0) : Integer.parseInt(cell2);
        return val1 + val2;
    }
}

###C#

public class Spreadsheet {
    private Dictionary<string, int> cellValues = new Dictionary<string, int>();

    public Spreadsheet(int size) {}

    public void SetCell(string cell, int value) {
        cellValues[cell] = value;
    }

    public void ResetCell(string cell) {
        cellValues.Remove(cell);
    }

    public int GetValue(string formula) {
        int i = formula.IndexOf('+');
        string cell1 = formula.Substring(1, i - 1);
        string cell2 = formula.Substring(i + 1);
        int val1 = char.IsLetter(cell1[0]) ? cellValues.GetValueOrDefault(cell1) : int.Parse(cell1);
        int val2 = char.IsLetter(cell2[0]) ? cellValues.GetValueOrDefault(cell2) : int.Parse(cell2);
        return val1 + val2;
    }
}

###Go

type Spreadsheet struct {
    cellValues map[string]int
}

func Constructor(rows int) Spreadsheet {
    return Spreadsheet{
cellValues: make(map[string]int),
}
}

func (this *Spreadsheet) SetCell(cell string, value int)  {
    this.cellValues[cell] = value
}

func (this *Spreadsheet) ResetCell(cell string)  {
    delete(this.cellValues, cell)
}

func (this *Spreadsheet) GetValue(formula string) int {
    i := strings.Index(formula, "+")
cell1 := formula[1:i]
cell2 := formula[i + 1:]

var val1, val2 int
if unicode.IsLetter(rune(cell1[0])) {
val1 = this.cellValues[cell1]
} else {
val1, _ = strconv.Atoi(cell1)
}
if unicode.IsLetter(rune(cell2[0])) {
val2 = this.cellValues[cell2]
} else {
val2, _ = strconv.Atoi(cell2)
}

return val1 + val2
}

###Python

class Spreadsheet:

    def __init__(self, rows: int):
        self.cell_values = {}

    def setCell(self, cell: str, value: int) -> None:
        self.cell_values[cell] = value

    def resetCell(self, cell: str) -> None:
        if cell in self.cell_values:
            del self.cell_values[cell]

    def getValue(self, formula: str) -> int:
        i = formula.find('+')
        cell1 = formula[1:i]
        cell2 = formula[i + 1:]
        val1 = self.cell_values.get(cell1, 0) if cell1[0].isalpha() else int(cell1)
        val2 = self.cell_values.get(cell2, 0) if cell2[0].isalpha() else int(cell2)
        return val1 + val2

###C

typedef struct {
    char *key;
    int val;
    UT_hash_handle hh;
} HashItem; 

HashItem *hashFindItem(HashItem **obj, const char *key) {
    HashItem *pEntry = NULL;
    HASH_FIND_STR(*obj, key, pEntry);
    return pEntry;
}

bool hashAddItem(HashItem **obj, char *key, int val) {
    if (hashFindItem(obj, key)) {
        return false;
    }
    HashItem *pEntry = (HashItem *)malloc(sizeof(HashItem));
    pEntry->key = strdup(key);
    pEntry->val = val;
    HASH_ADD_STR(*obj, key, pEntry);
    return true;
}

bool hashSetItem(HashItem **obj, char *key, int val) {
    HashItem *pEntry = hashFindItem(obj, key);
    if (!pEntry) {
        hashAddItem(obj, key, val);
    } else {
        pEntry->val = val;
    }
    return true;
}

int hashGetItem(HashItem **obj, const char *key, int defaultVal) {
    HashItem *pEntry = hashFindItem(obj, key);
    if (!pEntry) {
        return defaultVal;
    }
    return pEntry->val;
}

bool hashErase(HashItem **obj, char *key) {
    HashItem *pEntry = hashFindItem(obj, key);
    if (!pEntry) {
        return false;
    }
    HASH_DEL(*obj, pEntry);
    free(pEntry->key);
    free(pEntry);
    return true;
}

void hashFree(HashItem **obj) {
    HashItem *curr = NULL, *tmp = NULL;
    HASH_ITER(hh, *obj, curr, tmp) {
        HASH_DEL(*obj, curr);  
        free(curr->key);
        free(curr);             
    }
}

typedef struct {
    HashItem *cellValues;
} Spreadsheet;


Spreadsheet* spreadsheetCreate(int rows) {
    Spreadsheet *obj = (Spreadsheet *)malloc(sizeof(Spreadsheet));
    obj->cellValues = NULL;
    return obj;
}

void spreadsheetSetCell(Spreadsheet* obj, char* cell, int value) {
    hashSetItem(&obj->cellValues, cell, value);
}

void spreadsheetResetCell(Spreadsheet* obj, char* cell) {
    hashErase(&obj->cellValues, cell);
}

int getCellVal(Spreadsheet* obj, const char* cell) {
    if (isalpha(cell[0])) {
        return hashGetItem(&obj->cellValues, cell, 0);
    } else {
        return atoi(cell);
    }
}

int spreadsheetGetValue(Spreadsheet* obj, char* formula) {
    char* plus = strchr(formula, '+');
    char* cell1 = (char*)malloc(plus - formula);
    strncpy(cell1, formula + 1, plus - formula - 1);
    cell1[plus - formula - 1] = '\0';
    char* cell2 = strdup(plus + 1);
    int val1 = getCellVal(obj, cell1);
    int val2 = getCellVal(obj, cell2);
    free(cell1);
    free(cell2);
    return val1 + val2;
}

void spreadsheetFree(Spreadsheet* obj) {
    hashFree(&obj->cellValues);
    free(obj);
}

###JavaScript

var Spreadsheet = function(rows) {
    this.cellValues = {};
};

Spreadsheet.prototype.setCell = function(cell, value) {
    this.cellValues[cell] = value;
};

Spreadsheet.prototype.resetCell = function(cell) {
    delete this.cellValues[cell];
};

Spreadsheet.prototype.getValue = function(formula) {
    const i = formula.indexOf('+');
    const cell1 = formula.substring(1, i);
    const cell2 = formula.substring(i + 1);
    const val1 = /[a-z]/i.test(cell1[0]) ? (this.cellValues[cell1] || 0) : parseInt(cell1);
    const val2 = /[a-z]/i.test(cell2[0]) ? (this.cellValues[cell2] || 0) : parseInt(cell2);
    return val1 + val2;
};

###TypeScript

class Spreadsheet {
    private cellValues: {[key: string]: number} = {};

    constructor(size: number) {}

    setCell(cell: string, value: number): void {
        this.cellValues[cell] = value;
    }

    resetCell(cell: string): void {
        delete this.cellValues[cell];
    }

    getValue(formula: string): number {
        const i = formula.indexOf('+');
        const cell1 = formula.substring(1, i);
        const cell2 = formula.substring(i + 1);
        const val1 = /[a-z]/i.test(cell1[0]) ? (this.cellValues[cell1] || 0) : parseInt(cell1);
        const val2 = /[a-z]/i.test(cell2[0]) ? (this.cellValues[cell2] || 0) : parseInt(cell2);
        return val1 + val2;
    }
}

###Rust

use std::collections::HashMap;

struct Spreadsheet {
    cell_values: HashMap<String, i32>,
}

impl Spreadsheet {
    fn new(rows: i32) -> Self {
        Spreadsheet {
            cell_values: HashMap::new(),
        }
    }
    
    fn set_cell(&mut self, cell: String, value: i32) {
        self.cell_values.insert(cell, value);
    }
    
    fn reset_cell(&mut self, cell: String) {
        self.cell_values.remove(&cell);
    }
    
    fn get_value(&self, formula: String) -> i32 {
        let i = formula.find('+').expect("Formula must contain '+'");
        let (cell1, cell2) = formula.split_at(i);
        let cell1 = cell1[1..].to_string();
        let cell2 = cell2[1..].to_string();
        
        let val1 = if cell1.chars().next().unwrap().is_alphabetic() {
            *self.cell_values.get(&cell1).unwrap_or(&0)
        } else {
            cell1.parse().unwrap_or(0)
        };
        let val2 = if cell2.chars().next().unwrap().is_alphabetic() {
            *self.cell_values.get(&cell2).unwrap_or(&0)
        } else {
            cell2.parse().unwrap_or(0)
        };
        
        val1 + val2
    }
}

复杂度分析

  • 时间复杂度:所有操作的时间复杂度为 $O(1)$。每次操作时仅需查询和更新哈希表即可,其余操作仅涉及到解析字符串,需要的时间为 $O(1)$。

  • 空间复杂度:$O(\textit{cellsCount})$,其中 $\textit{cellsCount}$ 表示电子表格中的非 $0$ 元素个数,取决于方法 $\textit{setCell}$ 的调用次数。哈希表中最多有 $O(\textit{cellsCount})$ 个元素。

哈希表模拟,简洁写法(Python/Java/C++/Go/JS/Rust)

创建一个哈希表,直接把整个 $\textit{cell}$ 字符串作为哈希表的键,无需解析行号列号。

  • $\texttt{setCell}$:把键值对 $\textit{cell}$ 和 $\textit{value}$ 插入哈希表。
  • $\texttt{resetCell}$:把 $\textit{cell}$ 从哈希表中删除。
  • $\texttt{getValue}$:去掉第一个字符($\texttt{=}$),用 $\texttt{+}$ 分割字符串,得到两个字符串,分别转成整数再相加:
    • 如果字符串的第一个字符是大写字母,那么查找哈希表,得到对应的 $\textit{value}$(没有就是 $0$)。
    • 否则,把字符串转成整数。

###py

class Spreadsheet:
    def __init__(self, _):
        self.data = {}

    def setCell(self, cell: str, value: int) -> None:
        self.data[cell] = value

    def resetCell(self, cell: str) -> None:
        self.data.pop(cell, None)

    def getValue(self, formula: str) -> int:
        ans = 0
        for cell in formula[1:].split("+"):
            # 注:如果用 defaultdict(int),访问 self.data[cell] 也会把 cell 插入哈希表,增加空间复杂度
            ans += self.data.get(cell, 0) if cell[0].isupper() else int(cell)
        return ans

###java

class Spreadsheet {
    private final Map<String, Integer> data = new HashMap<>();

    public Spreadsheet(int rows) {
    }

    public void setCell(String cell, int value) {
        data.put(cell, value);
    }

    public void resetCell(String cell) {
        data.remove(cell);
    }

    public int getValue(String formula) {
        int ans = 0;
        for (String cell : formula.substring(1).split("\\+")) {
            if (Character.isUpperCase(cell.charAt(0))) {
                ans += data.getOrDefault(cell, 0);
            } else {
                ans += Integer.parseInt(cell);
            }
        }
        return ans;
    }
}

###cpp

class Spreadsheet {
    unordered_map<string, int> data;

public:
    Spreadsheet(int) {}

    void setCell(string cell, int value) {
        data[cell] = value;
    }

    void resetCell(string cell) {
        data.erase(cell);
    }

    int getValue(string formula) {
        int i = formula.find('+');
        string s = formula.substr(1, i - 1);
        string t = formula.substr(i + 1);
        // 注意 s 不在 data 中的时候,data[s] 会把 s 插入 data,这里从简没有判断
        return (isupper(s[0]) ? data[s] : stoi(s)) +
               (isupper(t[0]) ? data[t] : stoi(t));
    }
};

###go

type Spreadsheet map[string]int

func Constructor(int) Spreadsheet {
return Spreadsheet{}
}

func (s Spreadsheet) SetCell(cell string, value int) {
s[cell] = value
}

func (s Spreadsheet) ResetCell(cell string) {
delete(s, cell)
}

func (s Spreadsheet) GetValue(formula string) (ans int) {
for _, cell := range strings.Split(formula[1:], "+") {
if unicode.IsUpper(rune(cell[0])) {
ans += s[cell]
} else {
x, _ := strconv.Atoi(cell)
ans += x
}
}
return
}

###js

class Spreadsheet {
    constructor(rows) {
        this.data = new Map();
    }

    setCell(cell, value) {
        this.data.set(cell, value);
    }

    resetCell(cell) {
        this.data.delete(cell);
    }

    getValue(formula) {
        let ans = 0;
        for (const cell of formula.slice(1).split("+")) {
            if ("A" <= cell[0] && cell[0] <= "Z") {
                ans += this.data.get(cell) ?? 0;
            } else {
                ans += parseInt(cell);
            }
        }
        return ans;
    }
}

###rust

use std::collections::HashMap;

struct Spreadsheet {
    data: HashMap<String, i32>,
}

impl Spreadsheet {
    fn new(_: i32) -> Self {
        Spreadsheet {
            data: HashMap::new(),
        }
    }

    fn set_cell(&mut self, cell: String, value: i32) {
        self.data.insert(cell, value);
    }

    fn reset_cell(&mut self, cell: String) {
        self.data.remove(&cell);
    }

    fn get_value(&self, formula: String) -> i32 {
        formula[1..]
            .split('+')
            .map(|cell| {
                if cell.as_bytes()[0].is_ascii_uppercase() {
                    *self.data.get(cell).unwrap_or(&0)
                } else {
                    cell.parse::<i32>().unwrap()
                }
            })
            .sum()
    }
}

复杂度分析

  • 时间复杂度:初始化为 $\mathcal{O}(1)$,其余为 $\mathcal{O}(L)$,其中 $L$ 是 $\textit{cell}$(或者 $\textit{formula}$)的长度。
  • 空间复杂度:$\mathcal{O}(qL)$。其中 $q$ 为 $\texttt{setCell}$ 的调用次数。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

[Python3/Java/C++/Go/TypeScript] 一题一解:哈希表 + 有序集合(清晰题解)

方法一:哈希表 + 有序集合

我们用一个哈希表 $\text{d}$ 来存储任务信息,键为任务 ID,值为一个二元组 $(\text{userId}, \text{priority})$,表示该任务所属的用户 ID 以及任务的优先级。

我们用一个有序集合 $\text{st}$ 来存储当前系统中的所有任务,元素为一个二元组 $(-\text{priority}, -\text{taskId})$,表示任务的优先级和任务 ID 的相反数。我们将优先级和任务 ID 取相反数是为了让优先级最高且任务 ID 最大的任务在有序集合中排在最前面。

对于每个操作,我们可以按如下方式进行处理:

  • 初始化:对于每个任务 $(\text{userId}, \text{taskId}, \text{priority})$,我们将其添加到哈希表 $\text{d}$ 和有序集合 $\text{st}$ 中。
  • 添加任务:将任务 $(\text{userId}, \text{taskId}, \text{priority})$ 添加到哈希表 $\text{d}$ 和有序集合 $\text{st}$ 中。
  • 编辑任务:从哈希表 $\text{d}$ 中获取任务 ID对应的用户 ID 和旧优先级,然后从有序集合 $\text{st}$ 中删除旧的任务信息,再将新的任务信息添加到哈希表 $\text{d}$ 和有序集合 $\text{st}$ 中。
  • 删除任务:从哈希表 $\text{d}$ 中获取任务 ID 对应的优先级,然后从有序集合 $\text{st}$ 中删除任务信息,并从哈希表 $\text{d}$ 中删除任务。
  • 执行最高优先级任务:如果有序集合 $\text{st}$ 为空,返回 -1。否则,从有序集合 $\text{st}$ 中取出第一个元素,获取任务 ID,然后从哈希表 $\text{d}$ 中获取对应的用户 ID,并将任务从哈希表 $\text{d}$ 和有序集合 $\text{st}$ 中删除,最后返回用户 ID。

###python

class TaskManager:

    def __init__(self, tasks: List[List[int]]):
        self.d = {}
        self.st = SortedList()
        for task in tasks:
            self.add(*task)

    def add(self, userId: int, taskId: int, priority: int) -> None:
        self.d[taskId] = (userId, priority)
        self.st.add((-priority, -taskId))

    def edit(self, taskId: int, newPriority: int) -> None:
        userId, priority = self.d[taskId]
        self.st.discard((-priority, -taskId))
        self.d[taskId] = (userId, newPriority)
        self.st.add((-newPriority, -taskId))

    def rmv(self, taskId: int) -> None:
        _, priority = self.d[taskId]
        self.d.pop(taskId)
        self.st.remove((-priority, -taskId))

    def execTop(self) -> int:
        if not self.st:
            return -1
        taskId = -self.st.pop(0)[1]
        userId, _ = self.d[taskId]
        self.d.pop(taskId)
        return userId


# Your TaskManager object will be instantiated and called as such:
# obj = TaskManager(tasks)
# obj.add(userId,taskId,priority)
# obj.edit(taskId,newPriority)
# obj.rmv(taskId)
# param_4 = obj.execTop()

###java

class TaskManager {
    private final Map<Integer, int[]> d = new HashMap<>();
    private final TreeSet<int[]> st = new TreeSet<>((a, b) -> {
        if (a[0] == b[0]) {
            return b[1] - a[1];
        }
        return b[0] - a[0];
    });

    public TaskManager(List<List<Integer>> tasks) {
        for (var task : tasks) {
            add(task.get(0), task.get(1), task.get(2));
        }
    }

    public void add(int userId, int taskId, int priority) {
        d.put(taskId, new int[] {userId, priority});
        st.add(new int[] {priority, taskId});
    }

    public void edit(int taskId, int newPriority) {
        var e = d.get(taskId);
        int userId = e[0], priority = e[1];
        st.remove(new int[] {priority, taskId});
        st.add(new int[] {newPriority, taskId});
        d.put(taskId, new int[] {userId, newPriority});
    }

    public void rmv(int taskId) {
        var e = d.remove(taskId);
        int priority = e[1];
        st.remove(new int[] {priority, taskId});
    }

    public int execTop() {
        if (st.isEmpty()) {
            return -1;
        }
        var e = st.pollFirst();
        var t = d.remove(e[1]);
        return t[0];
    }
}

/**
 * Your TaskManager object will be instantiated and called as such:
 * TaskManager obj = new TaskManager(tasks);
 * obj.add(userId,taskId,priority);
 * obj.edit(taskId,newPriority);
 * obj.rmv(taskId);
 * int param_4 = obj.execTop();
 */

###cpp

class TaskManager {
private:
    unordered_map<int, pair<int, int>> d;
    set<pair<int, int>> st;

public:
    TaskManager(vector<vector<int>>& tasks) {
        for (const auto& task : tasks) {
            add(task[0], task[1], task[2]);
        }
    }

    void add(int userId, int taskId, int priority) {
        d[taskId] = {userId, priority};
        st.insert({-priority, -taskId});
    }

    void edit(int taskId, int newPriority) {
        auto [userId, priority] = d[taskId];
        st.erase({-priority, -taskId});
        st.insert({-newPriority, -taskId});
        d[taskId] = {userId, newPriority};
    }

    void rmv(int taskId) {
        auto [userId, priority] = d[taskId];
        st.erase({-priority, -taskId});
        d.erase(taskId);
    }

    int execTop() {
        if (st.empty()) {
            return -1;
        }
        auto e = *st.begin();
        st.erase(st.begin());
        int taskId = -e.second;
        int userId = d[taskId].first;
        d.erase(taskId);
        return userId;
    }
};

/**
 * Your TaskManager object will be instantiated and called as such:
 * TaskManager* obj = new TaskManager(tasks);
 * obj->add(userId,taskId,priority);
 * obj->edit(taskId,newPriority);
 * obj->rmv(taskId);
 * int param_4 = obj->execTop();
 */

###go

type TaskManager struct {
d  map[int][2]int
st *redblacktree.Tree[int, int]
}

func encode(priority, taskId int) int {
return (priority << 32) | taskId
}

func comparator(a, b int) int {
if a > b {
return -1
} else if a < b {
return 1
}
return 0
}

func Constructor(tasks [][]int) TaskManager {
tm := TaskManager{
d:  make(map[int][2]int),
st: redblacktree.NewWith[int, int](comparator),
}
for _, task := range tasks {
tm.Add(task[0], task[1], task[2])
}
return tm
}

func (this *TaskManager) Add(userId int, taskId int, priority int) {
this.d[taskId] = [2]int{userId, priority}
this.st.Put(encode(priority, taskId), taskId)
}

func (this *TaskManager) Edit(taskId int, newPriority int) {
if e, ok := this.d[taskId]; ok {
priority := e[1]
this.st.Remove(encode(priority, taskId))
this.d[taskId] = [2]int{e[0], newPriority}
this.st.Put(encode(newPriority, taskId), taskId)
}
}

func (this *TaskManager) Rmv(taskId int) {
if e, ok := this.d[taskId]; ok {
priority := e[1]
delete(this.d, taskId)
this.st.Remove(encode(priority, taskId))
}
}

func (this *TaskManager) ExecTop() int {
if this.st.Empty() {
return -1
}
it := this.st.Iterator()
it.Next()
taskId := it.Value()
if e, ok := this.d[taskId]; ok {
delete(this.d, taskId)
this.st.Remove(it.Key())
return e[0]
}
return -1
}

/**
 * Your TaskManager object will be instantiated and called as such:
 * obj := Constructor(tasks);
 * obj.Add(userId,taskId,priority);
 * obj.Edit(taskId,newPriority);
 * obj.Rmv(taskId);
 * param_4 := obj.ExecTop();
 */

###ts

class TaskManager {
    private d: Map<number, [number, number]>;
    private pq: PriorityQueue<[number, number]>;

    constructor(tasks: number[][]) {
        this.d = new Map();
        this.pq = new PriorityQueue<[number, number]>((a, b) => {
            if (a[0] === b[0]) {
                return b[1] - a[1];
            }
            return b[0] - a[0];
        });
        for (const task of tasks) {
            this.add(task[0], task[1], task[2]);
        }
    }

    add(userId: number, taskId: number, priority: number): void {
        this.d.set(taskId, [userId, priority]);
        this.pq.enqueue([priority, taskId]);
    }

    edit(taskId: number, newPriority: number): void {
        const e = this.d.get(taskId);
        if (!e) return;
        const userId = e[0];
        this.d.set(taskId, [userId, newPriority]);
        this.pq.enqueue([newPriority, taskId]);
    }

    rmv(taskId: number): void {
        this.d.delete(taskId);
    }

    execTop(): number {
        while (!this.pq.isEmpty()) {
            const [priority, taskId] = this.pq.dequeue();
            const e = this.d.get(taskId);
            if (e && e[1] === priority) {
                this.d.delete(taskId);
                return e[0];
            }
        }
        return -1;
    }
}

/**
 * Your TaskManager object will be instantiated and called as such:
 * var obj = new TaskManager(tasks)
 * obj.add(userId,taskId,priority)
 * obj.edit(taskId,newPriority)
 * obj.rmv(taskId)
 * var param_4 = obj.execTop()
 */

时间复杂度方面,初始化操作需要 $O(n \log n)$ 的时间,其中 $n$ 是初始任务的数量。每个添加、编辑、删除和执行操作都需要 $O(\log m)$ 的时间,其中 $m$ 是当前系统中的任务数量。由于总操作次数不超过 $2 \times 10^5$,因此整体时间复杂度是可接受的。空间复杂度 $O(n + m)$,用于存储哈希表和有序集合。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-设计任务管理器🟡

一个任务管理器系统可以让用户管理他们的任务,每个任务有一个优先级。这个系统需要高效地处理添加、修改、执行和删除任务的操作。

请你设计一个 TaskManager 类:

  • TaskManager(vector<vector<int>>& tasks) 初始化任务管理器,初始化的数组格式为 [userId, taskId, priority] ,表示给 userId 添加一个优先级为 priority 的任务 taskId 。

  • void add(int userId, int taskId, int priority) 表示给用户 userId 添加一个优先级为 priority 的任务 taskId ,输入 保证 taskId 不在系统中。

  • void edit(int taskId, int newPriority) 更新已经存在的任务 taskId 的优先级为 newPriority 。输入 保证 taskId 存在于系统中。

  • void rmv(int taskId) 从系统中删除任务 taskId 。输入 保证 taskId 存在于系统中。

  • int execTop() 执行所有用户的任务中优先级 最高 的任务,如果有多个任务优先级相同且都为 最高 ,执行 taskId 最大的一个任务。执行完任务后,taskId 从系统中 删除 。同时请你返回这个任务所属的用户 userId 。如果不存在任何任务,返回 -1 。

注意 ,一个用户可能被安排多个任务。

 

示例 1:

输入:
["TaskManager", "add", "edit", "execTop", "rmv", "add", "execTop"]
[[[[1, 101, 10], [2, 102, 20], [3, 103, 15]]], [4, 104, 5], [102, 8], [], [101], [5, 105, 15], []]

输出:
[null, null, null, 3, null, null, 5]

解释:

TaskManager taskManager = new TaskManager([[1, 101, 10], [2, 102, 20], [3, 103, 15]]); // 分别给用户 1 ,2 和 3 初始化一个任务。
taskManager.add(4, 104, 5); // 给用户 4 添加优先级为 5 的任务 104 。
taskManager.edit(102, 8); // 更新任务 102 的优先级为 8 。
taskManager.execTop(); // 返回 3 。执行用户 3 的任务 103 。
taskManager.rmv(101); // 将系统中的任务 101 删除。
taskManager.add(5, 105, 15); // 给用户 5 添加优先级为 15 的任务 105 。
taskManager.execTop(); // 返回 5 。执行用户 5 的任务 105 。

 

提示:

  • 1 <= tasks.length <= 105
  • 0 <= userId <= 105
  • 0 <= taskId <= 105
  • 0 <= priority <= 109
  • 0 <= newPriority <= 109
  • add ,edit ,rmv 和 execTop 的总操作次数 加起来 不超过 2 * 105 次。
  • 输入保证 taskId 是合法的。

3408. 设计任务管理器

解法一

思路和算法

任务管理器需要按照优先级降序和编号降序的顺序维护所有任务,并根据任务编号找到相应的任务并更新优先级。为了实现上述功能,需要维护如下信息。

  • 存储所有任务的有序集合,顺序为当优先级不同时按照优先级降序,当优先级相同时按照编号降序。

  • 存储任务编号与任务的对应关系的哈希表。

构造方法中,初始化有序集合与哈希表,将数组 $\textit{tasks}$ 中的所有任务加入有序集合并将任务编号与任务的对应关系存入哈希表。

对于 $\textit{add}$ 操作,根据用户编号 $\textit{userId}$、任务编号 $\textit{taskId}$ 和优先级 $\textit{priority}$ 创建任务,将该任务加入有序集合,并将任务编号 $\textit{taskId}$ 与该任务的对应关系存入哈希表。

对于 $\textit{edit}$ 操作,从哈希表中得到任务编号 $\textit{taskId}$ 对应的任务,将该任务从有序集合中移除,然后将该任务的优先级更新为 $\textit{newPriority}$,最后将该任务重新加入有序集合。

对于 $\textit{rmv}$ 操作,从哈希表中得到任务编号 $\textit{taskId}$ 对应的任务,将该任务从有序集合中移除,将任务编号 $\textit{taskId}$ 从哈希表中移除。

对于 $\textit{execTop}$ 操作,做法如下。

  • 当有序集合不为空时,得到有序集合中的首个任务并从有序集合中移除,将该任务的编号从哈希表中移除,返回该任务的用户编号。

  • 当有序集合为空时,不存在任何任务,返回 $-1$。

代码

###Java

class TaskManager {
    private TreeSet<Task> tasksSet;
    private Map<Integer, Task> idsTasks;

    public TaskManager(List<List<Integer>> tasks) {
        tasksSet = new TreeSet<Task>();
        idsTasks = new HashMap<Integer, Task>();
        for (List<Integer> task : tasks) {
            int userId = task.get(0), taskId = task.get(1), priority = task.get(2);
            add(userId, taskId, priority);
        }
    }

    public void add(int userId, int taskId, int priority) {
        Task task = new Task(userId, taskId, priority);
        tasksSet.add(task);
        idsTasks.put(taskId, task);
    }

    public void edit(int taskId, int newPriority) {
        Task task = idsTasks.get(taskId);
        tasksSet.remove(task);
        task.setPriority(newPriority);
        tasksSet.add(task);
    }

    public void rmv(int taskId) {
        Task task = idsTasks.get(taskId);
        tasksSet.remove(task);
        idsTasks.remove(taskId);
    }

    public int execTop() {
        if (!tasksSet.isEmpty()) {
            Task task = tasksSet.removeFirst();
            idsTasks.remove(task.getTaskId());
            return task.getUserId();
        } else {
            return -1;
        }
    }
}

class Task implements Comparable<Task> {
    private int userId;
    private int taskId;
    private int priority;

    public Task(int userId, int taskId, int priority) {
        this.userId = userId;
        this.taskId = taskId;
        this.priority = priority;
    }

    public int getUserId() {
        return userId;
    }

    public int getTaskId() {
        return taskId;
    }

    public int getPriority() {
        return priority;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public void setTaskId(int taskId) {
        this.taskId = taskId;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    @Override
    public int compareTo(Task task2) {
        return this.priority != task2.priority ? task2.priority - this.priority : task2.taskId - this.taskId;
    }
}

复杂度分析

  • 时间复杂度:构造方法的时间复杂度是 $O(n \log n)$,其余各项操作的时间复杂度是 $O(\log n)$,其中 $n$ 是任务总数。构造方法需要根据给定的任务列表更新有序集合与哈希表,其余各项操作需要对一项任务更新有序集合与哈希表,每项任务的更新时间是 $O(\log n)$。

  • 空间复杂度:$O(n)$,其中 $n$ 是任务总数。有序集合与哈希表的空间是 $O(n)$。

解法二

思路和算法

可以使用优先队列和延迟删除替代有序集合,实现任务管理器。优先队列的队首元素是优先级最高和编号最大的任务。

构造方法中,初始化优先队列与哈希表,将数组 $\textit{tasks}$ 中的所有任务加入优先队列并将任务编号与任务的对应关系存入哈希表。

对于 $\textit{add}$ 操作,根据用户编号 $\textit{userId}$、任务编号 $\textit{taskId}$ 和优先级 $\textit{priority}$ 创建任务,将该任务加入优先队列,并将任务编号 $\textit{taskId}$ 与该任务的对应关系存入哈希表。

对于 $\textit{edit}$ 操作,从哈希表中得到任务编号 $\textit{taskId}$ 对应的任务,新建一个用户编号和任务编号与原任务相同、优先级为 $\textit{newPriority}$ 的任务,将新任务加入优先队列,将任务编号 $\textit{taskId}$ 对应新任务存入哈希表。

对于 $\textit{rmv}$ 操作,将任务编号 $\textit{taskId}$ 从哈希表中移除。

对于 $\textit{execTop}$ 操作,做法如下。

  1. 当优先队列不为空且优先队列的队首元素应移除时,将优先队列的队首元素移除,重复该操作直到优先队列为空或优先队列的队首元素不应移除。优先队列的队首元素应移除的条件是以下两个条件之一成立。

    • 哈希表中不存在优先队列的队首元素对应的任务编号,该情况为任务被移除。

    • 优先队列的队首元素的优先级与优先队列的队首元素对应的任务编号对应的任务的用户编号不相等或优先级不相等,该情况为任务优先级更新过,优先队列的随后元素是优先级更新前的任务。

  2. 根据优先队列是否为空,执行相应的操作。

    • 当优先队列不为空时,将优先队列的队首元素的任务取出,将该任务的编号从哈希表中移除,返回该任务的用户编号。

    • 当优先队列为空时,不存在任何任务,返回 $-1$。

代码

###Java

class TaskManager {
    private PriorityQueue<Task> pq;
    private Map<Integer, Task> idsTasks;

    public TaskManager(List<List<Integer>> tasks) {
        pq = new PriorityQueue<Task>();
        idsTasks = new HashMap<Integer, Task>();
        for (List<Integer> task : tasks) {
            int userId = task.get(0), taskId = task.get(1), priority = task.get(2);
            add(userId, taskId, priority);
        }
    }

    public void add(int userId, int taskId, int priority) {
        Task task = new Task(userId, taskId, priority);
        pq.offer(task);
        idsTasks.put(taskId, task);
    }

    public void edit(int taskId, int newPriority) {
        Task task = idsTasks.get(taskId);
        Task newTask = new Task(task.getUserId(), taskId, newPriority);
        pq.offer(newTask);
        idsTasks.put(taskId, newTask);
    }

    public void rmv(int taskId) {
        idsTasks.remove(taskId);
    }

    public int execTop() {
        while (!pq.isEmpty() && shouldRemove(pq.peek())) {
            pq.poll();
        }
        if (!pq.isEmpty()) {
            Task task = pq.poll();
            idsTasks.remove(task.getTaskId());
            return task.getUserId();
        } else {
            return -1;
        }
    }

    private boolean shouldRemove(Task task) {
        return !idsTasks.containsKey(task.getTaskId()) || task.getUserId() != idsTasks.get(task.getTaskId()).getUserId() || task.getPriority() != idsTasks.get(task.getTaskId()).getPriority();
    }
}

class Task implements Comparable<Task> {
    private int userId;
    private int taskId;
    private int priority;

    public Task(int userId, int taskId, int priority) {
        this.userId = userId;
        this.taskId = taskId;
        this.priority = priority;
    }

    public int getUserId() {
        return userId;
    }

    public int getTaskId() {
        return taskId;
    }

    public int getPriority() {
        return priority;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public void setTaskId(int taskId) {
        this.taskId = taskId;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    @Override
    public int compareTo(Task task2) {
        return this.priority != task2.priority ? task2.priority - this.priority : task2.taskId - this.taskId;
    }
}

###C#

public class TaskManager {
    private PriorityQueue<Task, Task> pq;
    private IDictionary<int, Task> idsTasks;

    public TaskManager(IList<IList<int>> tasks) {
        pq = new PriorityQueue<Task, Task>();
        idsTasks = new Dictionary<int, Task>();
        foreach (IList<int> task in tasks) {
            int userId = task[0], taskId = task[1], priority = task[2];
            Add(userId, taskId, priority);
        }
    }

    public void Add(int userId, int taskId, int priority) {
        Task task = new Task(userId, taskId, priority);
        pq.Enqueue(task, task);
        if (!idsTasks.ContainsKey(taskId)) {
            idsTasks.Add(taskId, task);
        } else {
            idsTasks[taskId] = task;
        }
    }

    public void Edit(int taskId, int newPriority) {
        Task task = idsTasks[taskId];
        Task newTask = new Task(task.UserId, taskId, newPriority);
        pq.Enqueue(newTask, newTask);
        idsTasks[taskId] = newTask;
    }

    public void Rmv(int taskId) {
        idsTasks.Remove(taskId);
    }

    public int ExecTop() {
        while (pq.Count > 0 && ShouldRemove(pq.Peek())) {
            pq.Dequeue();
        }
        if (pq.Count > 0) {
            Task task = pq.Dequeue();
            idsTasks.Remove(task.TaskId);
            return task.UserId;
        } else {
            return -1;
        }
    }

    private bool ShouldRemove(Task task) {
        return !idsTasks.ContainsKey(task.TaskId) || task.UserId != idsTasks[task.TaskId].UserId || task.Priority != idsTasks[task.TaskId].Priority;
    }
}

class Task : IComparable<Task> {
    public int UserId { get; set; }
    public int TaskId { get; set; }
    public int Priority { get; set; }

    public Task(int userId, int taskId, int priority) {
        UserId = userId;
        TaskId = taskId;
        Priority = priority;
    }

    public int CompareTo(Task task2) {
        return this.Priority != task2.Priority ? task2.Priority - this.Priority : task2.TaskId - this.TaskId;
    }
}

复杂度分析

  • 时间复杂度:构造方法的时间复杂度是 $O(n \log n)$,方法 $\textit{add}$ 和 $\textit{edit}$ 的时间复杂度是 $O(\log n)$,方法 $\textit{rmv}$ 的时间复杂度是 $O(1)$,方法 $\textit{execTop}$ 的均摊时间复杂度是 $O(\log n)$,其中 $n$ 是任务总数。构造方法需要根据给定的任务列表更新优先队列与哈希表,方法 $\textit{add}$ 和 $\textit{edit}$ 需要对一项任务更新优先队列与哈希表,每项任务的更新时间是 $O(\log n)$,方法 $\textit{rmv}$ 更新哈希表的时间是 $O(1)$,方法 $\textit{execTop}$ 需要执行延迟删除,每个元素最多加入优先队列和从优先队列中取出各一次因此均摊时间是 $O(\log n)$。

  • 空间复杂度:$O(n)$,其中 $n$ 是任务总数。优先队列与哈希表的空间是 $O(n)$。

懒删除堆(Python/Java/C++/Go/JS/Rust)

为了实现 $\texttt{execTop}$,用最大堆维护优先级、任务编号、用户编号。

为了实现堆的懒更新和懒删除,用一个哈希表记录每个任务编号对应的最新的优先级和用户编号。哈希表的 key 是任务编号,value 是优先级和用户编号组成的二元组。

  • $\texttt{edit}$:直接把一个新的优先级、任务编号、用户编号三元组入堆。同时更新哈希表。
  • $\texttt{rmv}$:把元素从哈希表中删掉。更简单的写法是,把优先级改成 $-1$。
  • $\texttt{execTop}$:不断弹出「货不对板」的堆顶,也就是任务编号和哈希表中记录的数据不一致的堆顶。直到堆为空或者找到一致的数据。

注 1:如果你学过 Dijkstra 算法,其中的堆就是懒更新堆。

注 2:题目保证输入的 $\textit{tasks}$ 数组中没有重复的 $\textit{taskId}$。

###py

class TaskManager:
    def __init__(self, tasks: List[List[int]]):
        self.mp = {taskId: (priority, userId) for userId, taskId, priority in tasks}
        self.h = [(-priority, -taskId, userId) for userId, taskId, priority in tasks]  # 取相反数,变成最大堆
        heapify(self.h)

    def add(self, userId: int, taskId: int, priority: int) -> None:
        self.mp[taskId] = (priority, userId)
        heappush(self.h, (-priority, -taskId, userId))

    def edit(self, taskId: int, newPriority: int) -> None:
        # 懒修改
        self.add(self.mp[taskId][1], taskId, newPriority)

    def rmv(self, taskId: int) -> None:
        # 懒删除
        self.mp[taskId] = (-1, -1)

    def execTop(self) -> int:
        while self.h:
            priority, taskId, userId = heappop(self.h)
            if self.mp[-taskId] == (-priority, userId):
                self.rmv(-taskId)
                return userId
            # else 货不对板,堆顶和 mp 中记录的不一样,说明堆顶数据已被修改或删除,不做处理
        return -1

###java

class TaskManager {
    // taskId -> (priority, userId)
    private final Map<Integer, int[]> mp = new HashMap<>();

    // (priority, taskId, userId)
    private final PriorityQueue<int[]> pq =
            new PriorityQueue<>((a, b) -> a[0] != b[0] ? b[0] - a[0] : b[1] - a[1]);

    public TaskManager(List<List<Integer>> tasks) {
        for (List<Integer> task : tasks) {
            add(task.get(0), task.get(1), task.get(2));
        }
    }

    public void add(int userId, int taskId, int priority) {
        mp.put(taskId, new int[]{priority, userId});
        pq.offer(new int[]{priority, taskId, userId});
    }

    public void edit(int taskId, int newPriority) {
        // 懒修改
        int userId = mp.get(taskId)[1];
        add(userId, taskId, newPriority);
    }

    public void rmv(int taskId) {
        // 懒删除
        mp.get(taskId)[0] = -1;
    }

    public int execTop() {
        while (!pq.isEmpty()) {
            int[] top = pq.poll();
            int priority = top[0], taskId = top[1], userId = top[2];
            int[] p = mp.get(taskId);
            if (p[0] == priority && p[1] == userId) {
                rmv(taskId);
                return userId;
            }
            // else 货不对板,堆顶和 mp 中记录的不一样,说明堆顶数据已被修改或删除,不做处理
        }
        return -1;
    }
}

###cpp

class TaskManager {
    unordered_map<int, pair<int, int>> mp; // taskId -> (priority, userId)
    priority_queue<tuple<int, int, int>> pq; // (priority, taskId, userId)

public:
    TaskManager(vector<vector<int>>& tasks) {
        for (auto& task : tasks) {
            add(task[0], task[1], task[2]);
        }
    }

    void add(int userId, int taskId, int priority) {
        mp[taskId] = {priority, userId};
        pq.emplace(priority, taskId, userId);
    }

    void edit(int taskId, int newPriority) {
        // 懒修改
        add(mp[taskId].second, taskId, newPriority);
    }

    void rmv(int taskId) {
        // 懒删除
        mp[taskId].first = -1;
    }

    int execTop() {
        while (!pq.empty()) {
            auto [priority, taskId, userId] = pq.top();
            pq.pop();
            if (mp[taskId] == pair(priority, userId)) {
                rmv(taskId);
                return userId;
            }
            // else 货不对板,堆顶和 mp 中记录的不一样,说明堆顶数据已被修改或删除,不做处理
        }
        return -1;
    }
};

###go

type pair struct{ priority, userId int }

type TaskManager struct {
mp map[int]pair // taskId -> (priority, userId)
h  *hp          // (priority, taskId, userId)
}

func Constructor(tasks [][]int) TaskManager {
n := len(tasks)
mp := make(map[int]pair, n) // 预分配空间
h := make(hp, n)
for i, t := range tasks {
userId, taskId, priority := t[0], t[1], t[2]
mp[taskId] = pair{priority, userId}
h[i] = tuple{priority, taskId, userId}
}
heap.Init(&h)
return TaskManager{mp, &h}
}

func (t *TaskManager) Add(userId, taskId, priority int) {
t.mp[taskId] = pair{priority, userId}
heap.Push(t.h, tuple{priority, taskId, userId})
}

func (t *TaskManager) Edit(taskId, newPriority int) {
// 懒修改
t.Add(t.mp[taskId].userId, taskId, newPriority)
}

func (t *TaskManager) Rmv(taskId int) {
// 懒删除
t.mp[taskId] = pair{-1, -1}
}

func (t *TaskManager) ExecTop() int {
for t.h.Len() > 0 {
top := heap.Pop(t.h).(tuple)
priority, taskId, userId := top.priority, top.taskId, top.userId
if t.mp[taskId] == (pair{priority, userId}) {
t.Rmv(taskId)
return userId
}
// else 货不对板,堆顶和 mp 中记录的不一样,说明堆顶数据已被修改或删除,不做处理
}
return -1
}

type tuple struct{ priority, taskId, userId int }
type hp []tuple
func (h hp) Len() int           { return len(h) }
func (h hp) Less(i, j int) bool { return cmp.Or(h[i].priority-h[j].priority, h[i].taskId-h[j].taskId) > 0 }
func (h hp) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *hp) Push(v any)        { *h = append(*h, v.(tuple)) }
func (h *hp) Pop() any          { a := *h; v := a[len(a)-1]; *h = a[:len(a)-1]; return v }

###js

class TaskManager {
    constructor(tasks) {
        // taskId -> [priority, userId]
        this.mp = new Map();

        // 最大堆 [priority, taskId, userId]
        this.pq = new PriorityQueue((a, b) => a[0] !== b[0] ? b[0] - a[0] : b[1] - a[1]);

        for (const [userId, taskId, priority] of tasks) {
            this.add(userId, taskId, priority);
        }
    }

    add(userId, taskId, priority) {
        this.mp.set(taskId, [priority, userId]);
        this.pq.enqueue([priority, taskId, userId]);
    }

    edit(taskId, newPriority) {
        // 懒修改
        const userId = this.mp.get(taskId)[1];
        this.add(userId, taskId, newPriority);
    }

    rmv(taskId) {
        // 懒删除
        this.mp.get(taskId)[0] = -1;
    }

    execTop() {
        while (!this.pq.isEmpty()) {
            const [priority, taskId, userId] = this.pq.dequeue();
            const [p, u] = this.mp.get(taskId);
            if (p === priority && u === userId) {
                this.rmv(taskId);
                return userId;
            }
            // else 货不对板,堆顶和 mp 中记录的不一样,说明堆顶数据已被修改或删除,不做处理
        }
        return -1;
    }
}

###rust

use std::collections::{BinaryHeap, HashMap};

struct TaskManager {
    mp: HashMap<i32, (i32, i32)>, // taskId -> (priority, userId)
    h: BinaryHeap<(i32, i32, i32)>, // (priority, taskId, userId)
}

impl TaskManager {
    fn new(tasks: Vec<Vec<i32>>) -> Self {
        let mut manager = Self {
            mp: HashMap::new(),
            h: BinaryHeap::new(),
        };
        for task in tasks {
            manager.add(task[0], task[1], task[2]);
        }
        manager
    }

    fn add(&mut self, user_id: i32, task_id: i32, priority: i32) {
        self.mp.insert(task_id, (priority, user_id));
        self.h.push((priority, task_id, user_id));
    }

    fn edit(&mut self, task_id: i32, new_priority: i32) {
        // 懒修改
        let user_id = self.mp.get(&task_id).unwrap().1;
        self.add(user_id, task_id, new_priority);
    }

    fn rmv(&mut self, task_id: i32) {
        // 懒删除
        *self.mp.get_mut(&task_id).unwrap() = (-1, -1);
    }

    fn exec_top(&mut self) -> i32 {
        while let Some((priority, task_id, user_id)) = self.h.pop() {
            let (p, u) = self.mp[&task_id];
            if p == priority && u == user_id {
                self.rmv(task_id);
                return user_id;
            }
            // else 货不对板,堆顶和 mp 中记录的不一样,说明堆顶数据已被修改或删除,不做处理
        }
        -1
    }
}

复杂度分析

  • 时间复杂度:
    • 初始化:$\mathcal{O}(n)$ 或者 $\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{tasks}$ 的长度。Python 和 Go 使用了堆化,复杂度是 $\mathcal{O}(n)$ 的。
    • $\texttt{add}$ 和 $\texttt{edit}$:$\mathcal{O}(\log (n+q))$,其中 $q$ 是 $\texttt{add}$ 和 $\texttt{edit}$ 的调用次数之和。
    • $\texttt{rmv}$:$\mathcal{O}(1)$。
    • $\texttt{execTop}$:均摊 $\mathcal{O}(\log (n+q))$。每个元素至多入堆出堆各一次。
  • 空间复杂度:$\mathcal{O}(n+q)$。

专题训练

见下面数据结构题单的「§5.6 懒删除堆」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

设计数字容器系统(双哈希表与二进制树)

按照题目的要求,数值和索引的取值范围都是[1, 10^9]
要能够在指定的索引处赋值某个数值或修改某个数值,以及找出某个数值所对应的所有索引中最小的那个。那么,对于设计出的数据结构,就有几方面的要求。

  1. 在取值范围内的每个索引,要么还没有存放任何数值,否则只能对应一个数值。
  2. 在取值范围内的每个数值,如果还没有被放入任何索引(也可能是中途被覆盖掉了),则查不出它的任何索引值,即返回无效值-1,否则,它可以对应有多个索引值,且要求能够在较快的时间内找出这些索引值当中的最小值。

这里采用两个哈希表,来处理索引和数值之间的对应关系。另外采用一个二进制树,来存储一个数值所对应的可能的多个索引值。

  1. 索引的哈希表,键值就是索引本身。由于一个索引最多只能对应一个数值,所以,除了键值本身以外,哈希表里面还存储索引值对应的数值。
  2. 数值的哈希表,键值就是数值本身。除了键值以外,哈希表里面还存储一棵二进制树,这棵树里面存储有可能多个的索引值。
    二进制树的具体操作,下面再描述。
  3. 开始创建对象时,两个哈希表都初始化为空表。
  4. 在往某个索引修改数值时,分下面几种情况进行处理。
    4.1 如果这个索引还没有存储过任何数值,则新建一个索引哈希节点。哈希节点里面的数值就等于新赋值的number
    (4.1.1) 如果这个数值对应的数值哈希节点不存在,说明是新出现的数值,则新建一个数值哈希节点,哈希节点中的二进制树也新建,把该索引值存储到二进制树中。
    (4.1.2) 如果这个数值对应的数值哈希节点已经存在,则把当前这个索引值,直接加到它的二进制树中。
    4.2 如果这个索引已经有存储过的数值。
    (4.2.1) 如果新赋值的数值,等于该索引处旧的数值,则什么都不会发生变化,不做任何处理。
    (4.2.2) 如果新赋值的数值,不等于该索引处旧的数值,则先把该索引值,从旧数值的数值哈希节点对应的二进制树中抹去(如果二进制树里面唯一的索引值被抹去了,则这个数值哈希节点也对应地要删除)。再看新的数值对应的数值哈希节点是否存在。
    >>> 4.2.2.1 如果新的数值对应的数值哈希节点不存在,说明是新出现的数值,则新建一个数值哈希节点,哈希节点中的二进制树也新建,把该索引值存储到二进制树中。
    >>> 4.2.2.2 如果新的数值对应的数值哈希节点已经存在,则把当前这个索引值,直接加到它的二进制树中。

【二进制树】
二进制树其实就是一棵二叉树,如下图所示,该数据结构将数值以二进制的方式进行处理,包括添加、删除、查询最大最小值(或者第k大值)等等。
本题中,取值范围[1, 10^9]内的数,最多可能占据30个bits,但为了方便解释,这里以一共4个bits的数值为例(即图中所示),本质道理上是一样的。
从最低位bit0到最高位bit3,共4个bits。假设现在要存储2, 3, 5, 10, 11这几个数值到二进制树中去。
对这些数值,它们的二进制(保留4个bits)依次为0010, 0011, 0101, 1010, 1011,各自从最高的bit3开始进行处理。

  1. 最高位是1的,从根节点往右走,最高位是0的,从根节点往左走,然后分别给各自的左右节点的计数加一。
  2. 然后是bit2,同上,该位是1的,往当前节点的右分支走,该位是0的,往当前节点的左分支走。同时,分别给各自的左右节点的计数加一。
  3. 依次类推,直到bit0为止。
  4. 沿途过程中,如果往左或往右时,对应的分支为NULL,则新建一个分支即可。
  5. 直到最后bit0结束后,每一个数值,都在二进制树中留下了自己的痕迹。上述的2, 3, 5, 10, 11这几个值,在一共4个bits的二进制树留下的痕迹,就如图所示。

相反的,如果需要从二进制树中移除某个数值,其过程和添加数值相反看待就可以,即,上面的计数加一,改成了计数减一。当某个分支的计数值减为零的时候,该分支删除,并赋为NULL即可。

当需要查询整棵二进制树中的最小值时,只要从根节点开始往下,每一个bit依次查看,能往左走的(即左分支不为NULL),尽量往左走,除非左分支为NULL,才往右分支走。按照这个逻辑,走到最低的bit0时,就是最小值的路径,依次把各个bit赋值上,就是结果。在下图的示例中,从根节点开始往下查询,一开始从根节点往下查看bit3时,往左,从bit3往下查看bit2时,也往左,从bit2往下查看bit1时,发现只能往右,此时结果的bit1置为1,最后从bit1往下查看bit0时,还是往左,这样到结束时,得到的二进制结果就是0010,即十进制的2

截图_16587109215246.png

时间复杂度:O(N*logE),每个哈希节点的添加、删除、修改处理的时间复杂度是O(1),二进制树中添加、删除、查询的时间复杂度是O(logE)
空间复杂度:O(N*logE),索引哈希表的空间复杂度是O(N),数值哈希表中由于附带了二进制树,其空间复杂度是O(N*logE)
其中,N是总的操作次数,E表示数值和索引的取值范围[1, 10^9]

#define INVALID_VALUE -1
#define HIGHEST_BIT 0x20000000

/* 二进制树的节点定义。 */
typedef struct BinaryNode_s
{
    int counter;
    struct BinaryNode_s *left;
    struct BinaryNode_s *right;
}
BinaryNode;

/* 数值哈希节点的定义。 */
typedef struct
{
    int number;
    BinaryNode *binary;
    UT_hash_handle hh;
}
HashNumberNode;

/* 索引哈希节点的定义。 */
typedef struct
{
    int index;
    int number;
    UT_hash_handle hh;
}
HashIndexNode;

/* 对象中存储两个哈希表。 */
typedef struct
{
    HashIndexNode *indexTable;
    HashNumberNode *numberTable;
}
NumberContainers;

/* 几个自定义函数的声明,具体实现见下。 */
extern void insertIntoBinary(BinaryNode *binary, int index);
extern void removeFromBinary(BinaryNode *binary, int index);
extern int findSmallestFromBinary(BinaryNode *binary);
extern void deleteTree(BinaryNode *binary);

/* 创建对象,初始化时两个哈希表均为空。 */
NumberContainers *numberContainersCreate()
{
    NumberContainers *obj = (NumberContainers *)malloc(sizeof(NumberContainers));
    obj->indexTable = NULL;
    obj->numberTable = NULL;
    return obj;
}

/* 某个索引处进行赋值。 */
void numberContainersChange(NumberContainers *obj, int index, int number)
{
    HashIndexNode *indexNode = NULL;
    HashNumberNode *numberNode = NULL;
    HASH_FIND_INT(obj->indexTable, &index, indexNode);
    /* 如果该索引是第一次赋值。 */
    if(NULL == indexNode)
    {
        /* 新建一个索引哈希节点。 */
        indexNode = (HashIndexNode *)malloc(sizeof(HashIndexNode));
        indexNode->index = index;
        indexNode->number = number;
        HASH_ADD_INT(obj->indexTable, index, indexNode);
        /* 查询这个赋值的数值,对应的数值哈希节点是否已经存在。 */
        HASH_FIND_INT(obj->numberTable, &number, numberNode);
        if(NULL == numberNode)
        {
            /* 如果还不存在,则先新建一个数值哈希节点。 */
            numberNode = (HashNumberNode *)malloc(sizeof(HashNumberNode));
            numberNode->number = number;
            /* 二进制树的根节点,是默认存在的,先创建好。
            这里使用calloc代替malloc,是暗含了初始化counter为0,和左右分支为NULL的过程。 */
            numberNode->binary = (BinaryNode *)calloc(1, sizeof(BinaryNode));
            HASH_ADD_INT(obj->numberTable, number, numberNode);
        }
        /* 将索引值加入到对应的二进制树中。 */
        insertIntoBinary(numberNode->binary, index);
    }
    /* 否则,只有当新的数值不等于旧的数值时,才有必要进行处理,否则什么都不会发生变化。 */
    else if(indexNode->number != number)
    {
        /* 从旧的数值对应的二进制树中,把这个索引值删除。 */
        HASH_FIND_INT(obj->numberTable, &indexNode->number, numberNode);
        if(NULL != numberNode)
        {
            removeFromBinary(numberNode->binary, index);
            /* 如果删除这个索引值之后,这个数值不剩下任何索引值了,则表示谁也不要它了,它就可以伤心地离开了。 */
            if(NULL == numberNode->binary->left && NULL == numberNode->binary->right)
            {
                HASH_DEL(obj->numberTable, numberNode);
                free(numberNode->binary);
                free(numberNode);
            }
        }
        /* 把新的数值赋值给该索引哈希节点。 */
        indexNode->number = number;
        /* 同时查看新的数值,对应的数值哈希节点是否已经存在。 */
        HASH_FIND_INT(obj->numberTable, &number, numberNode);
        if(NULL == numberNode)
        {
            /* 如果不存在,则新建数值的哈希节点。 */
            numberNode = (HashNumberNode *)malloc(sizeof(HashNumberNode));
            numberNode->number = number;
            /* 同上,根节点默认存在,且使用calloc代替malloc申请空间。 */
            numberNode->binary = (BinaryNode *)calloc(1, sizeof(BinaryNode));
            HASH_ADD_INT(obj->numberTable, number, numberNode);
        }
        /* 把这个索引值加到新的数值哈希节点对应的二进制树中。 */
        insertIntoBinary(numberNode->binary, index);
    }
    return;
}

/* 查找某个数值对应的最小索引值。 */
int numberContainersFind(NumberContainers *obj, int number)
{
    int x = INVALID_VALUE;
    HashNumberNode *numberNode = NULL;
    HASH_FIND_INT(obj->numberTable, &number, numberNode);
    /* 当这个数值哈希节点存在时,才可能找到最小索引值,否则就返回初始化的-1。 */
    if(NULL != numberNode)
    {
        x = findSmallestFromBinary(numberNode->binary);
    }
    return x;
}

/* 释放对象。先释放两个哈希表,再释放对象。 */
void numberContainersFree(NumberContainers *obj)
{
    HashIndexNode *indexNode = NULL, *it = NULL;
    HashNumberNode *numberNode = NULL, *nt = NULL;
    HASH_ITER(hh, obj->indexTable, indexNode, it)
    {
        HASH_DEL(obj->indexTable, indexNode);
        free(indexNode);
    }
    HASH_ITER(hh, obj->numberTable, numberNode, nt)
    {
        HASH_DEL(obj->numberTable, numberNode);
        if(NULL != numberNode->binary)
        {
            deleteTree(numberNode->binary);
        }
        free(numberNode);
    }
    free(obj);
    return;
}

/* 几个自定义函数的实现,见下。 */

/* 将索引值加入到对应的二进制树中。 */
void insertIntoBinary(BinaryNode *binary, int index)
{
    int bit = HIGHEST_BIT;
    BinaryNode *node = binary;
    /* 从最高bit开始,逐位往下。 */
    while(0 != bit)
    {
        /* 该位为1的,往右分支走。且计数加一。 */
        if(index & bit)
        {
            if(NULL == node->right)
            {
                node->right = (BinaryNode *)calloc(1, sizeof(BinaryNode));
            }
            node->right->counter++;
            node = node->right;
        }
        /* 该位为0的,往左分支走。且计数加一。 */
        else
        {
            if(NULL == node->left)
            {
                node->left = (BinaryNode *)calloc(1, sizeof(BinaryNode));
            }
            node->left->counter++;
            node = node->left;
        }
        /* 下一位。 */
        bit = bit >> 1;
    }
    return;
}

/* 从二进制树中删除某个索引值。 */
void removeFromBinary(BinaryNode *binary, int index)
{
    int bit = HIGHEST_BIT;
    BinaryNode *node = binary;
    /* 从最高bit开始,逐位往下。 */
    while(0 != bit && NULL != node)
    {
        /* 该位为1的,往右分支走。且计数减一。 */
        if(index & bit)
        {
            node->right->counter--;
            if(0 == node->right->counter)
            {
                deleteTree(node->right);
                node->right = NULL;
            }
            node = node->right;
        }
        /* 该位为0的,往左分支走。且计数减一。 */
        else
        {
            node->left->counter--;
            if(0 == node->left->counter)
            {
                deleteTree(node->left);
                node->left = NULL;
            }
            node = node->left;
        }
        /* 下一位。 */
        bit = bit >> 1;
    }
    return;
}

/* 在二进制树中查找最小的索引值。 */
int findSmallestFromBinary(BinaryNode *binary)
{
    int bit = HIGHEST_BIT, x = 0;
    BinaryNode *node = binary;
    /* 从最高bit开始,逐位往下。 */
    while(0 != bit && NULL != node)
    {
        /* 能往左就尽量往左,左边的数值小。 */
        if(NULL != node->left)
        {
            node = node->left;
        }
        /* 没有左分支的话,就只能往右了。往右时,该bit为1,要加上。这里用或操作代替加法也可以。 */
        else
        {
            x += bit;
            node = node->right;
        }
        /* 下一位。 */
        bit = bit >> 1;
    }
    return x;
}

/* 删除树,或某个子树。 */
void deleteTree(BinaryNode *binary)
{
    if(NULL != binary->left)
    {
        deleteTree(binary->left);
    }
    if(NULL != binary->right)
    {
        deleteTree(binary->right);
    }
    free(binary);
    return;
}

每日一题-设计数字容器系统🟡

设计一个数字容器系统,可以实现以下功能:

  • 在系统中给定下标处 插入 或者 替换 一个数字。
  • 返回 系统中给定数字的最小下标。

请你实现一个 NumberContainers 类:

  • NumberContainers() 初始化数字容器系统。
  • void change(int index, int number) 在下标 index 处填入 number 。如果该下标 index 处已经有数字了,那么用 number 替换该数字。
  • int find(int number) 返回给定数字 number 在系统中的最小下标。如果系统中没有 number ,那么返回 -1 。

 

示例:

输入:
["NumberContainers", "find", "change", "change", "change", "change", "find", "change", "find"]
[[], [10], [2, 10], [1, 10], [3, 10], [5, 10], [10], [1, 20], [10]]
输出:
[null, -1, null, null, null, null, 1, null, 2]

解释:
NumberContainers nc = new NumberContainers();
nc.find(10); // 没有数字 10 ,所以返回 -1 。
nc.change(2, 10); // 容器中下标为 2 处填入数字 10 。
nc.change(1, 10); // 容器中下标为 1 处填入数字 10 。
nc.change(3, 10); // 容器中下标为 3 处填入数字 10 。
nc.change(5, 10); // 容器中下标为 5 处填入数字 10 。
nc.find(10); // 数字 10 所在的下标为 1 ,2 ,3 和 5 。因为最小下标为 1 ,所以返回 1 。
nc.change(1, 20); // 容器中下标为 1 处填入数字 20 。注意,下标 1 处之前为 10 ,现在被替换为 20 。
nc.find(10); // 数字 10 所在下标为 2 ,3 和 5 。最小下标为 2 ,所以返回 2 。

 

提示:

  • 1 <= index, number <= 109
  • 调用 change 和 find 的 总次数 不超过 105 次。

竞赛时 时间紧,临时加一层缓存就好AC了 这样会快很多

批注 2022-07-24 185846.png

var NumberContainers = function() {
    this.map = new Map()
    this.obj = {}
    this.cache = {}
};


NumberContainers.prototype.change = function(index, number) {
   if(this.obj[index] !== undefined){
       let v = this.obj[index]
       let set = this.map.get(v)
       if(set) set.delete(index)
       delete this.cache[v]
   }
    this.obj[index] = number 
    this.map.set(number, (this.map.get(number) || new Set()).add(index))
    delete this.cache[number]
};

NumberContainers.prototype.find = function(number) {
    if(this.cache[number]) return this.cache[number]
    let set = this.map.get(number),ret 
    if(!set || set.size === 0) ret = -1
    else ret = Math.min(...set)
    this.cache[number] = ret
    return ret
};


两种方法:有序集合 / 懒删除堆(Python/Java/C++/Go)

方法一:哈希表 + 有序集合

为了实现 $\texttt{find}$,我们需要对每个 $\textit{number}$ 创建一个有序集合,维护这个 $\textit{number}$ 对应的所有下标。用有序集合可以快速地获取最小下标。

对于 $\texttt{change}$,如果 $\textit{index}$ 处有数字,我们需要先删除旧的数字,所以还需要知道每个 $\textit{index}$ 对应的 $\textit{number}$ 是多少,这可以用一个哈希表记录。

具体来说,创建一个哈希表 $\textit{indexToNumber}$,以及一个哈希表套有序集合 $\textit{numberToIndices}$。

对于 $\texttt{change}$:

  • 如果 $\textit{index}$ 处有数字 $x$,那么从 $\textit{numberToIndices}[x]$ 中删除 $\textit{index}$(删除旧的数据)。
  • 然后,更新(或者插入)$\textit{indexToNumber}[\textit{index}] = \textit{number}$,往 $\textit{numberToIndices}[\textit{number}]$ 中添加 $\textit{index}$。

对于 $\texttt{find}$,获取 $\textit{numberToIndices}[\textit{number}]$ 中的最小元素即可。

class NumberContainers:
    def __init__(self):
        self.index_to_number = {}
        # from sortedcontainers import SortedSet
        self.number_to_indices = defaultdict(SortedSet)

    def change(self, index: int, number: int) -> None:
        # 移除旧数据
        old_number = self.index_to_number.get(index, None)
        if old_number is not None:
            self.number_to_indices[old_number].discard(index)

        # 添加新数据
        self.index_to_number[index] = number
        self.number_to_indices[number].add(index)

    def find(self, number: int) -> int:
        indices = self.number_to_indices[number]
        return indices[0] if indices else -1
class NumberContainers {
    private final Map<Integer, Integer> indexToNumber = new HashMap<>();
    private final Map<Integer, TreeSet<Integer>> numberToIndices = new HashMap<>();

    public void change(int index, int number) {
        // 移除旧数据
        Integer oldNumber = indexToNumber.get(index);
        if (oldNumber != null) {
            numberToIndices.get(oldNumber).remove(index);
        }

        // 添加新数据
        indexToNumber.put(index, number);
        numberToIndices.computeIfAbsent(number, _ -> new TreeSet<>()).add(index);
    }

    public int find(int number) {
        TreeSet<Integer> indices = numberToIndices.get(number);
        return indices == null || indices.isEmpty() ? -1 : indices.first();
    }
}
class NumberContainers {
    unordered_map<int, int> index_to_number;
    unordered_map<int, set<int>> number_to_indices;

public:
    void change(int index, int number) {
        // 移除旧数据
        auto it = index_to_number.find(index);
        if (it != index_to_number.end()) {
            number_to_indices[it->second].erase(index);
        }

        // 添加新数据
        index_to_number[index] = number;
        number_to_indices[number].insert(index);
    }

    int find(int number) {
        auto it = number_to_indices.find(number);
        return it == number_to_indices.end() || it->second.empty() ? -1 : *it->second.begin();
    }
};
// import "github.com/emirpasic/gods/v2/trees/redblacktree"
type NumberContainers struct {
indexToNumber   map[int]int
numberToIndices map[int]*redblacktree.Tree[int, struct{}]
}

func Constructor() NumberContainers {
return NumberContainers{map[int]int{}, map[int]*redblacktree.Tree[int, struct{}]{}}
}

func (n NumberContainers) Change(index, number int) {
// 移除旧数据
if oldNumber, ok := n.indexToNumber[index]; ok {
n.numberToIndices[oldNumber].Remove(index)
}

// 添加新数据
n.indexToNumber[index] = number
if n.numberToIndices[number] == nil {
n.numberToIndices[number] = redblacktree.New[int, struct{}]()
}
n.numberToIndices[number].Put(index, struct{}{})
}

func (n NumberContainers) Find(number int) int {
indices, ok := n.numberToIndices[number]
if !ok || indices.Empty() {
return -1
}
return indices.Left().Key
}

复杂度分析

  • 时间复杂度:
    • 初始化 $\mathcal{O}(1)$。
    • $\texttt{change}$:$\mathcal{O}(\log q)$,其中 $q$ 是 $\texttt{change}$ 的调用次数。
    • $\texttt{find}$:$\mathcal{O}(\log q)$ 或者 $\mathcal{O}(1)$,取决于有序集合是否额外维护最小值。
  • 空间复杂度:$\mathcal{O}(q)$。

方法二:哈希表 + 懒删除堆

$\textit{numberToIndices}$ 改成哈希表套最小堆。

对于 $\texttt{change}$,不删除旧数据。

对于 $\texttt{find}$,查看堆顶是否等于 $\textit{number}$,若不相同,则意味着堆顶是之前没有删除的旧数据,弹出堆顶;否则堆顶就是答案。

class NumberContainers:
    def __init__(self):
        self.index_to_number = {}
        self.number_to_indices = defaultdict(list)

    def change(self, index: int, number: int) -> None:
        # 添加新数据
        self.index_to_number[index] = number
        heappush(self.number_to_indices[number], index)

    def find(self, number: int) -> int:
        indices = self.number_to_indices[number]
        while indices and self.index_to_number[indices[0]] != number:
            heappop(indices)  # 堆顶货不对板,说明是旧数据,删除
        return indices[0] if indices else -1
class NumberContainers {
    private final Map<Integer, Integer> indexToNumber = new HashMap<>();
    private final Map<Integer, PriorityQueue<Integer>> numberToIndices = new HashMap<>();

    public void change(int index, int number) {
        // 添加新数据
        indexToNumber.put(index, number);
        numberToIndices.computeIfAbsent(number, _ -> new PriorityQueue<>()).offer(index);
    }

    public int find(int number) {
        PriorityQueue<Integer> indices = numberToIndices.get(number);
        if (indices == null) {
            return -1;
        }
        while (!indices.isEmpty() && indexToNumber.get(indices.peek()) != number) {
            indices.poll(); // 堆顶货不对板,说明是旧数据,删除
        }
        return indices.isEmpty() ? -1 : indices.peek();
    }
}
class NumberContainers {
    unordered_map<int, int> index_to_number;
    unordered_map<int, priority_queue<int, vector<int>, greater<int>>> number_to_indices;

public:
    void change(int index, int number) {
        // 添加新数据
        index_to_number[index] = number;
        number_to_indices[number].push(index);
    }

    int find(int number) {
        auto& indices = number_to_indices[number];
        while (!indices.empty() && index_to_number[indices.top()] != number) {
            indices.pop(); // 堆顶货不对板,说明是旧数据,删除
        }
        return indices.empty() ? -1 : indices.top();
    }
};
type NumberContainers struct {
indexToNumber   map[int]int
numberToIndices map[int]*hp
}

func Constructor() NumberContainers {
return NumberContainers{map[int]int{}, map[int]*hp{}}
}

func (n NumberContainers) Change(index, number int) {
// 添加新数据
n.indexToNumber[index] = number
if _, ok := n.numberToIndices[number]; !ok {
n.numberToIndices[number] = &hp{}
}
heap.Push(n.numberToIndices[number], index)
}

func (n NumberContainers) Find(number int) int {
indices, ok := n.numberToIndices[number]
if !ok {
return -1
}
for indices.Len() > 0 && n.indexToNumber[indices.IntSlice[0]] != number {
heap.Pop(indices) // 堆顶货不对板,说明是旧数据,删除
}
if indices.Len() == 0 {
return -1
}
return indices.IntSlice[0]
}

type hp struct{ sort.IntSlice }
func (h *hp) Push(v any) { h.IntSlice = append(h.IntSlice, v.(int)) }
func (h *hp) Pop() any   { a := h.IntSlice; v := a[len(a)-1]; h.IntSlice = a[:len(a)-1]; return v }

复杂度分析

  • 时间复杂度:
    • 初始化 $\mathcal{O}(1)$。
    • $\texttt{change}$:$\mathcal{O}(\log q)$,其中 $q$ 是 $\texttt{change}$ 的调用次数。
    • $\texttt{find}$:均摊 $\mathcal{O}(\log q)$。
  • 空间复杂度:$\mathcal{O}(q)$。

专题训练

见下面数据结构题单的「§5.6 懒删除堆」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

「三哈希构造」Java 80ms

方法:「哈希构造」

截屏2022-07-24 07.23.12.png

思路及算法

双向映射很容易想到, 用哈希表保存 索引 和 数值的关系. 关键是一旦出现了 同下标的更新 应该如何处理
我们用 numMap 保存正向 索引 —> 数值, 而 数值 —> 索引 我们拆为

  • minIndex 表示 —> 最小索引 的映射
  • indexsList 表示 —> 所有索引 的映射

原因在于最小索引需要频繁更新, 而找最小索引只有当最小索引被删了才需要遍历更新(发生频率低)

具体逻辑见代码

代码

###java

class Solution {
  class NumberContainers {

    HashMap<Integer, Integer> numMap = new HashMap<>();//保存正向 索引 —> 数值
    HashMap<Integer, Integer> minIndex = new HashMap<>();//保存反向 数值 —> 最小索引 的映射
    HashMap<Integer, Set<Integer>> indexsList = new HashMap<>();//保存反向 数值 —> 所有索引 的映射

    public NumberContainers() {

    }

    public void change(int index, int number) {
      if (numMap.containsKey(index)) {//1.如果存在index
        int prev = numMap.remove(index);
        Set<Integer> prevIndexsList = indexsList.get(prev);
        prevIndexsList.remove(index);
        if (prevIndexsList.isEmpty()) {//只有一个数, 删完了, 需要清空key
          indexsList.remove(prev);
          minIndex.remove(prev);
        } else if (minIndex.get(prev) == index) {//如果删的就是最小索引,需要再找一次最小索引
          Set<Integer> indexs = indexsList.get(prev);
          int min = Integer.MAX_VALUE;
          for (int x : indexs) min = Math.min(min, x);
          minIndex.put(prev, min);
        }
      }
      numMap.put(index, number);//更新正向映射
      Integer min = minIndex.getOrDefault(number, Integer.MAX_VALUE);
      min = Math.min(min, index);
      minIndex.put(number, min);//更新 最小索引 映射
      Set<Integer> list = indexsList.getOrDefault(number, new HashSet<>());
      list.add(index);
      indexsList.put(number, list);//更新 所有索引 映射
    }

    public int find(int number) {
      return minIndex.getOrDefault(number, -1);//返回相当简洁
    }
  }
}

结语
如果对您有帮助,欢迎点赞、收藏、关注 沪里户外,让更多的小伙伴看到,祝大家offer多多,AC多多

❌