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("");
}
相关推荐
weixin_382395235 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__5 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.6 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~7 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华8 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子10 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅10 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni10 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学12 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端