Q51- code295- 数据流的中位数 + Q52- code767- 重构字符串

Q51- code295- 数据流的中位数

实现思路

1 本题特点:

  • 是 动态地 添加数字
  • 需要随时能够获取中位数
  • 数据是持续流入的

2 暴力/直观 解法

  • 维护一个有序数组,每次插入时保持数组有序
  • 获取中位数时,直接访问中间位置
  • 但这样插入操作的时间复杂度是 O(n),因为插入时 要移动元素
  • 为什么要移动元素:因为我们要保持整个数组都有序
  • 关键问题:我们真的需要整个数组都有序吗?

3.1 思考中位数的 定义/本质:

  • 中位数的定义是什么?是把数据分成相等两部分的 那个数
  • 对于有序数组 [1, 2, 3, 4, 5],中位数是 3
  • 重要观察:3 左边的数都比 3 小,右边的数都比 3 大, 3 是中位数
  • 更重要的观察:左边具体是 1, 2 还是 2, 1 的顺序并不重要,右边是 4, 5 还是 5, 4 的顺序也不重要
  • 所以,我们只需要 "找到中位数" 即可,不需要整个数组都有序

3.2 也就是说,我们只需要知道

  • 左半部分的最大值(因为这可能是中位数)
  • 右半部分的最小值(因为这也可能是中位数)
  • 其他数字的具体顺序都不重要

3.3 引入堆的契机:

  • 需求1:快速获取一组数中的 最大值/最小值
  • 需求2:快速增删元素
  • 这正是堆数据结构的特长!
  • 大顶堆可以快速获取最大值,小顶堆可以快速获取最小值

4 总结以上 思维方式:

  • 当发现一个直观解法效率不够时
  • 回到问题本质,看看是否 真的需要所有的操作
  • 找到关键信息,用更高效的数据结构来维护
text 复制代码
直观解法:[1, 2, 3, 4, 5] 完全有序数组
                ↓
优化思路:只需要知道 [1, 2] 中的最大值和 [4, 5] 中的最小值
                ↓
最终方案:大顶堆 [1, 2] 和小顶堆 [3, 4, 5]

参考文档

01- 方法1参考文档

代码实现

1 方法1: 最大堆 && 最小堆

  • 时间复杂度: addNum O(logq)
    • q 是 addNum 的调用次数
  • 空间复杂度: O(q)
ts 复制代码
class minHeap<T> {
  private data: T[];
  private compare: (a: T, b: T) => boolean;

  constructor(compare: typeof this.compare) {
    this.compare = compare;
    this.data = [];
  }

  // 使用 getter 替代方法
  get size() {
    return this.data.length;
  }

  get peek() {
    return this.data[0];
  }

  enque(item: T) {
    this.data.push(item);
    this.siftUp(this.size - 1);
  }

  deque() {
    // 易错点1:如果堆为空,直接返回,防止堆中误进入'undefined'
    if (this.size === 0) return;
    // 易错点2:只有一个的时候直接出队,防止后续操作数组长度不会减少
    if (this.size === 1) return this.data.pop();
    const ret = this.data[0];
    this.data[0] = this.data.pop();
    this.siftDown(0);
    return ret;
  }

  private siftUp(idx: number) {
    while (idx > 0) {
      const pdx = (idx - 1) >> 1;
      // compre为true: a < b; 即 cur < parent
      const willUp = pdx >= 0 && this.compare(this.data[idx], this.data[pdx]);
      if (!willUp) break;
      this.swap(idx, pdx);
      idx = pdx;
    }
  }

  private siftDown(idx: number) {
    while (1) {
      let ldx = idx * 2 + 1, rdx = ldx + 1;
      let ndx = idx;
      if (ldx < this.size && this.compare(this.data[ldx], this.data[ndx]))
        ndx = ldx;
      if (rdx < this.size && this.compare(this.data[rdx], this.data[ndx]))
        ndx = rdx;
      if (ndx === idx) break;
      this.swap(idx, ndx);
      idx = ndx;
    }
  }

  private swap(i: number, j: number) {
    [this.data[i], this.data[j]] = [this.data[j], this.data[i]];
  }
}

class MedianFinder {
  // 较小部分的 最大堆: 通过compare来控制反转为 最大堆
  private left: minHeap<number>;
  // 较大部分的 最小堆
  private right: minHeap<number>;

  constructor() {
    // 保证 left.size === right.size 或者 left.size === right.size + 1
    this.left = new minHeap((a, b) => a > b);
    this.right = new minHeap((a, b) => a < b);
  }

  addNum(num: number): void {
    // 成员个数相等,统一逻辑以简化代码:进右出最小的,进左
    if (this.left.size === this.right.size) {
      this.right.enque(num);
      this.left.enque(this.right.deque());
    } else {
      // left.size === right.size + 1,统一逻辑以简化代码:进左出最大的,进右
      this.left.enque(num);
      this.right.enque(this.left.deque());
    }
  }

  findMedian(): number {
    return this.left.size > this.right.size
      ? this.left.peek
      : (this.right.peek + this.left.peek) / 2;
  }
}

Q52- code767- 重构字符串

实现思路

1 举简单例子,尝试找到规律

例1:"aab" 字符频率:a=2, b=1 手动排列:"aba" ✓ 观察:最多字符a出现2次,总长度3

例2:"aaab" 字符频率:a=3, b=1 手动尝试:"aaba" → 失败,有相邻的a 观察:最多字符a出现3次,总长度4

例3:"aabbcc" 字符频率:a=2, b=2, c=2 手动排列:"abacbc" ✓ 观察:所有字符频率相等

2 尝试总结规律

  • 成功的例子:最多字符频率 ≤ 总长度的一半左右
  • 失败的例子:最多字符频率 > 总长度的一半
  • 为什么:
    • 如果字符a出现次数超过 (n + 1) / 2
    • 那么无论怎么排列,a都会相邻
    • 因为偶数位置不够放所有的a

即:如果某个字母的 最大频率 > (字符串长度 + 1) / 2,则无解

  • 为什么是 (n+1)/2
    • 对于长度为4的字符串:最多允许2个相同字符
    • 对于长度为5的字符串:最多允许3个相同字符
    • 公式:(n+1)/2

3 如果是有解的,如何构造

方法1:贪心思想- 每次选择 剩余次数最多的字符

  • 如果每次都选最多的,能最大化利用空间
  • 每次取频率最高的,如果和前一个相同,取第二高的】
  • 具体实现:优先队列

方法2:位置填充

  • 按频率从大到小,先填偶数位置,再填奇数位置
  • 先把最多的字符放在偶数位置 (0,2,4...)
  • 再把其他字符填充剩余位置

为什么这样构造不会出错(正确性证明):反证法

假设填到某个奇数位置时,出现了相邻相同的情况

比如

text 复制代码
位置:0 1 2 3 4 5 6
当前:a b a b a ? ?

如果第5个位置填a,就会和位置4的a相邻。

但是! 如果字符a能填到位置5,说明:

  • a已经填了位置0,2,4(3个位置)
  • 还要填位置5(第4个位置)
  • 所以a至少出现4次

但是! 我们假设了最大频率 ≤ ⌈n/2⌉

  • 对于n=7,⌈7/2⌉ = 4
  • 所以a最多出现4次
  • 如果a出现4次,它只能填偶数位置(0,2,4,6)
  • 不可能填到奇数位置5

矛盾! 所以不可能出现相邻相同的情况

方法3: 桶计数 + 优先放置最多频率值

  • 桶排序(计数排序): 统计最大频率 + 最大字符
  • S2 判断可行性
  • S3 优先放置 最大频率字符 + 去除该字符
  • S4 隔位置 放置其他字符即可

参考文档

01- 方法1参考文档

代码实现

1 方法1: 排序 + 偶数位放置

  • 时间复杂度:O(N + KlogK)

    • N是 字符串长度
    • K是 不同字符的数量,K ≤ 26
  • 空间复杂度: O(N)

ts 复制代码
function reorganizeString(s: string): string {
  // S1: 频率统计
  const len = s.length;
  const record = [...s].reduce((map, str) => {
    map.set(str, (map.get(str) ?? 0) + 1);
    return map;
  }, new Map<string, number>());

  // S2: 排序获取最大频率
  const sorted = [...record.entries()].sort((a, b) => b[1] - a[1]);
  const maxFreq = sorted[0][1];

  // S3: 判断是否可行
  if (maxFreq > (len + 1) >> 1) return "";

  // S4: 填充结果
  let res = [], pos = 0;
  for (let [str, freq] of sorted) {
    for (let i = 0; i < freq; i++) {
      if (pos >= len) pos = 1;
      res[pos] = str;
      pos += 2;
    }
  }
  return res.join("");
}

方法2 桶计数 + 优先放置最多频率值

  • 时间复杂度:O(n)
    • 统计频率:O(n) - 遍历字符串一次
    • 放置最大频率字符:O(maxFreq) ≤ O(n)
    • 放置其他字符:O(26 + 剩余字符数) = O(n)
    • 总计:O(n)
  • 空间复杂度:O(n)
    • buckets数组:O(26) = O(1) - 常数空间
    • 结果数组:O(n) - 存储重构后的字符串
    • 其他变量:O(1)
    • 总计:O(n)
ts 复制代码
function reorganizeString(s: string): string {
  const n = s.length;
  const buckets = new Array(26).fill(0);
  let maxFreq = 0, maxIdx = 0;

  // 统计频率并找最大频率字符
  for (const ch of s) {
    const idx = ch.charCodeAt(0) - 97;
    buckets[idx]++;
    if (buckets[idx] > maxFreq) {
      maxFreq = buckets[idx];
      maxIdx = idx;
    }
  }
  // 判断可行性
  if (maxFreq > (n + 1) >> 1) return "";

  const res = new Array(n);
  let pos = 0;

  // 先放最大频率字符到偶数位置
  while (buckets[maxIdx] > 0) {
    res[pos] = String.fromCharCode(maxIdx + 97);
    pos += 2;
    buckets[maxIdx]--;
  }

  // 放其他字符
  for (let j = 0; j < 26; j++) {
    while (buckets[j] > 0) {
      // 切换到奇数位置
      if (pos >= n) pos = 1;
      res[pos] = String.fromCharCode(j + 97);
      pos += 2;
      buckets[j]--;
    }
  }
  return res.join("");
}
相关推荐
JuneXcy10 分钟前
11.Layout-Pinia优化重复请求
前端·javascript·css
子洋20 分钟前
快速目录跳转工具 zoxide 使用指南
前端·后端·shell
天下无贼!21 分钟前
【自制组件库】从零到一实现属于自己的 Vue3 组件库!!!
前端·javascript·vue.js·ui·架构·scss
CF14年老兵42 分钟前
✅ Next.js 渲染速查表
前端·react.js·next.js
司宸1 小时前
学习笔记八 —— 虚拟DOM diff算法 fiber原理
前端
阳树阳树1 小时前
JSON.parse 与 JSON.stringify 可能引发的问题
前端
让辣条自由翱翔1 小时前
总结一下Vue的组件通信
前端
dyb1 小时前
开箱即用的Next.js SSR企业级开发模板
前端·react.js·next.js
前端的日常1 小时前
Vite 如何处理静态资源?
前端
前端的日常1 小时前
如何在 Vite 中配置路由?
前端