算法:数组和链表双解法求连续中值

486 阅读4分钟

连续中值

这是我参与2022首次更文挑战的第27天,活动详情查看:2022首次更文挑战」。

正文

面试题 17.20. 连续中值

随机产生数字并传递给一个方法。你能否完成这个方法,在每次产生新值时,寻找当前所有值的中间值(中位数)并保存。

中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

  • void addNum(int num) - 从数据流中添加一个整数到数据结构中。
  • double findMedian() - 返回目前所有元素的中位数。

示例:

addNum(1)
addNum(2)
findMedian() -> 1.5
addNum(3) 
findMedian() -> 2

解析

这是一道leetcode 困难难度的算法题,但是个人觉得并没有那么困难。设计数据结构和方法考察了你对问题解决的思路和设计。

设计思路

求中位数,目标是求最中间的数,那么就少不了存储数据和排序。涉及到多个数据的存储以及排序,那么设计的第一想法肯定是数组。所以实现构造函数就是设定一个存储数据的数组。

addNum方法就是将数据添加到数组中,值得注意的是,这里并不会对所有的数组进行排序,而是将插入的数据,按照大小插入到指定位置中即可,如果每一个数据都是按照顺序进行插入的,那么这个数组一定就是排序完成的数组。

findMedian方法就是找到中位数,一个已经排好序的数组想要找到中位数的解法的简单程度不言而喻。

实现构造函数

var MedianFinder = function() {
    this.dataList = [] // 数值数组
};

这里只需要设置一个接收所有数据的数组

实现 addNum

/** 
 * @param {number} num
 * @return {void}
 */
MedianFinder.prototype.addNum = function(num) {
    // 插入
    if (this.dataList.length === 0) {
        this.dataList.push(num)
    } else {
        let insertIndex = this.dataList.length
        for (let index = 0 ; index < this.dataList.length; index++) {
            if (this.dataList[index] > num) {
                insertIndex = index // 插入位置
                break
            }
        }
        this.dataList.splice(insertIndex, 0 , num)
    }
};

利用插入的原理实现排序。

实现 findMedian

 * @return {number}
 */
MedianFinder.prototype.findMedian = function() {
    if (this.dataList.length % 2 === 0) {
        // 偶数
        const index = this.dataList.length / 2 - 1
        return (this.dataList[index] + this.dataList[index + 1]) / 2
    } else {
        return  this.dataList[parseInt(this.dataList.length / 2)]
        
    }
};

分别对数组个数为偶数和奇数做出不同的处理。

完整代码:

/**
 * initialize your data structure here.
 */
var MedianFinder = function () {
  this.dataList = [] // 数值数组
};

/** 
* @param {number} num
* @return {void}
*/
MedianFinder.prototype.addNum = function (num) {
  // 插入
  if (this.dataList.length === 0) {
    this.dataList.push(num)
  } else {
    let insertIndex = this.dataList.length
    for (let index = 0; index < this.dataList.length; index++) {
      if (this.dataList[index] > num) {
        insertIndex = index // 插入位置
        break
      }
    }
    this.dataList.splice(insertIndex, 0, num)
  }
};

/**
* @return {number}
*/
MedianFinder.prototype.findMedian = function () {
  if (this.dataList.length % 2 === 0) {
    // 偶数
    const index = this.dataList.length / 2 - 1
    return (this.dataList[index] + this.dataList[index + 1]) / 2
  } else {
    return this.dataList[parseInt(this.dataList.length / 2)]

  }
};

/**
* Your MedianFinder object will be instantiated and called as such:
* var obj = new MedianFinder()
* obj.addNum(num)
* var param_2 = obj.findMedian()
*/

提交!

image.png

使用链表

上述算法使用到了在数组中插入的操作,通常在数组中插入数据的操作会比较繁琐,原因是既要找到插入位置,又要讲所有插入位置后的数据往后移动一位。在 JS中我们是可以通过 splice方法去解决,如果在通用的算法中,这里我们就比较合适的应用就是链表。

1. 创建链表数据结构

在JS 中是没有链表这种数据结构的,所以需要我们自己写一个构造函数

var LinkList = function (val, next) {
    this.val = val
    this.next = next || null
}

2. 实现构造函数

var MedianFinder = function() {
    this.linkList = new LinkList(Number.MIN_VALUE, null) // 创建一个链表,并且创建一个虚拟节点
    this.length = 0
};

这里的构造函数中,存放数据的是链表结构,第一个节点我们并不知道是什么数据,所以使用最小值作为虚拟节点。 由于链表要想知道长度就必须遍历,在操作过程中为了知道长度去遍历是比较消耗内存的,所以我们可以单独设置一个长度字段,用来记录长度。

3.实现 addNum

/** 
 * @param {number} num
 * @return {void}
 */
MedianFinder.prototype.addNum = function(num) {
    // 遍历链表 插入数值
    let isAdded = false
    let p = this.linkList
    while(p.next && !isAdded) {
        const next = p.next
        if (next.val >= num) {
            p.next = new LinkList(num, next)
            isAdded = true
            p = p.next
        }
        p = p.next
    }
    if (!isAdded) {
        p.next = new LinkList(num, null)
    }
    this.length++
};

链表的优势在这里体现,插入数值比数组性能上要优一些。每插入一次数据,记得 length++

3.实现 findMedian

/**
 * @return {number}
 */
MedianFinder.prototype.findMedian = function() {
    let index = 0 
    let p = this.linkList.next
    while(index < this.length / 2 -1) {
        index++
        p = p.next
    }
    return this.length % 2 === 0 ?  (p.val + p.next.val) / 2 : p.val
};

这里的原理和数组一致,遍历取中值。

4. 完整代码:

/**
 * initialize your data structure here.
 */

var LinkList = function (val, next) {
    this.val = val
    this.next = next || null
}

var MedianFinder = function() {
    this.linkList = new LinkList(Number.MIN_VALUE, null) // 创建一个链表,并且创建一个虚拟节点
    this.length = 0
};

/** 
 * @param {number} num
 * @return {void}
 */
MedianFinder.prototype.addNum = function(num) {
    // 遍历链表 插入数值
    let isAdded = false
    let p = this.linkList
    while(p.next && !isAdded) {
        const next = p.next
        if (next.val >= num) {
            p.next = new LinkList(num, next)
            isAdded = true
            p = p.next
        }
        p = p.next
    }
    if (!isAdded) {
        p.next = new LinkList(num, null)
    }
    this.length++
};

/**
 * @return {number}
 */
MedianFinder.prototype.findMedian = function() {
    let index = 0 
    let p = this.linkList.next
    while(index < this.length / 2 -1) {
        index++
        p = p.next
    }
    return this.length % 2 === 0 ?  (p.val + p.next.val) / 2 : p.val
};

/**
 * Your MedianFinder object will be instantiated and called as such:
 * var obj = new MedianFinder()
 * obj.addNum(num)
 * var param_2 = obj.findMedian()
 */