【数据结构与算法(十九)】

题目

数据流中的中位数

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

思路

1、由于数据从一个数据流中都出来的,因而数据的数目随时间的变化而增加。如果用一个数据容器来保存从流中都出来的数据,则当有新的数据从流中读出来时,这些数据就插入数据容器。
2、现在需要考虑的是要用什么数据容器
3、数组是最简单的数据容器。如果数组没有排序,则可以用Partition函数找出数组中的中位数。在没有排序的数组中插入一个数字和找出中位数的时间复杂度分别是O(1)O(n)
4、可以在往数组中插入新数据时让数组保持排序,这时由于可能需要移动O(n)个数,因此需要O(n)时间才能完成插入操作。在已经排好序的数组找出中位数是一个简单的操作,只需要O(1)时间即可完成。
5、排序的链表。需要O(n)时间才能在链表中找到合适的位置插入新的数据。如果定义两个指针指向链表中间的节点(如果链表的节点数目是奇数,那么这两个指针指向同一个节点),那么可以在O(1)时间内得出中位数。此时的时间复杂度与基于排序的数组的时间复杂度是一样的
6、二叉搜索树可以把插入新数据的平均时间降低到O(log n)。但是,当二叉搜索树极度不平衡从而看起来像一个排序的链表时,插入新数据的时间仍然是O(n)。为了得到中位数,可以在二叉树节点中添加一个表示子树节点数目的字段。有了这个字段,可以在平均O(log n)时间内得到中位数,但是最差情况仍然需要O(n)时间【把每个数都遍历】
7、为了避免二叉搜索树的最差情况,还可以利用平衡的二叉搜索树,即AVL。通常AVL树的平衡因子是左、右子树的高度差。可以稍作修改,把AVL树的平衡因子改为左、右子树节点数目之差。有了这个改动,可以用O(log n)时间往AVL树种添加一个新节点,同时用O(1)时间得到所有节点的中位数
8、AVL树的时间效率很高,但大部分编程语言的函数库都没有实现这个数据结构。所以在短时间内较难实现。
9、如果数据在容器种已经排序,那么中位数可以由P1和P2指向的数得到。如果容器中数据的数目是奇数,那么P1和P2指向同一个数据。数据容器被分隔成两部分。位于容器左边部分的数据比右边部分的数据小。并且要使得P1指向的数据是左边部分 最大的数,P2指向的数据是右边部分最小的数。
10、如果能保证数据容器左边的数据都小于右边的数据,那么即使左、右两边内部的数据没有排序,也可以根据左边最大的数及右边最小的数得到中位数。如何快速从一个数据容器中找出最大数?用最大堆实现这个数据容器,因为位于堆顶的就是最大的数据。同样,也可以快速从最小堆中找出最小数
11、【总结9和10】用一个最大堆实现左边的数据容器,用一个最小堆实现右边的数据容器。往堆中插入一个数据的时间效率是O(log n)。由于只需要O(1)时间就可以得到位于堆顶的数据,因此得到中位数的时间复杂度是O(1)
12、总结以上所有讨论的数据结构:没有排序的数组、排序的数组、二叉搜索树、AVL树、最大堆和最小堆等不同数据结构的时间复杂度

数据结构插入的时间复杂度得到中位数的时间复杂度
没有排序的数组O(1)O(n)
排序的数组O(n)O(1)
排序的链表O(n)O(1)
二叉搜索树平均O(log n),最差O(n)平均O(log n),最差O(n)
AVL树O(log n)O(1)
最大堆和最小堆O(log n)O(1)

13、接下来考虑最大堆和最小堆实现的一些细节。首先要保证数据平均分配到两个堆中,因此两个堆中数据的数目之差不能超过1.为了实现平均分配,可以在数据的总数目是偶数时把新数据插入最小堆,否则插入最大堆。还要保证最大堆中的所有数据都小于最小堆中的数据。当数据的总数目是偶数时,按照前面的分配规则把新的数据插入最小堆。如果此时这个新的数据比最大堆中的一些数据要小,可以把这个新数据插到最大堆,接着把最大堆中最大的数字拿出来插入到最小堆。由于最终插入最小堆的数字是原最大堆中最大的数字,这样就保证了最小堆中所有数字都大于最大堆中数字。

#include<functional>
//基于STL中的函数push_heap、pop_heap以及vector实现堆
template<typename T> class DynamicArray
{
public:
    //插入从数据流中得到的数据
    void Insert(T num) {
        //如果是偶数个,把它放在最小堆中
        //偶数个说明没有插入前,最小堆个数=最大堆个数
        //这里默认规定是最小堆的个数比最大堆多一个,就是说先放到最小堆中
        if (((min.size() + max.size()) & 1) == 0)
        {
            //如果最大堆(存着较小的数)中的最大的数max[0]大于新插入的数
            //说明目前最大堆中最大的数还不够小,要把它拿出来才行
            if (max.size() > 0 && num < max[0]) {
                //把新数据插入到最大堆中,因为它比较小
                max.push_back(num);
                push_heap(max.begin(), max.end(), less<T>());
                num = max[0];
                //把原来最大堆中最大的数弹出来
                //less<T>():表示按从大到小递减排序
                pop_heap(max.begin(), max.end(), less<T>());
                max.pop_back();
            }
            //按从小到大递增插入num,所以min[0]是最小的数
            min.push_back(num);
            push_heap(min.begin(), min.end(), greater<T>());
        }//如果是奇数
        else {
            if (min.size() > 0 && min[0] < num) {
                min.push_back(num);
                push_heap(min.begin(), min.end(), greater<T>());
                num = min[0];
                pop_heap(min.begin(), min.end(), greater<T>());
                min.pop_back();
            }
            max.push_back(num);
            push_heap(max.begin(), max.end(), less<T>());
        }
    }
    //得到已有数据的中位数
    T GetMedian()
    {
        int size = min.size() + max.size();
        if (size == 0)
            throw exception("数据流中没有数字");

        T median = 0;
        if ((size & 1) == 1)
            median = min[0];
        else
            median = (min[0] + max[0]) / 2;

        return median;
    }
private:
    vector<T> max;//最大堆
    vector<T> min;//最小堆
};

连续子数组的最大和

输入一个整数数组,数组中有正数也有负数。数组中一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)

思路

解法1:举例分析数组的规律【例子{1,-2,3,10,-4,7,2,-5}】

1、从头到尾逐个累加。初始化和为0,第一步加上第一个数字,此时和为1.第二步加上数字-2,和就变成了-1.第三部加上数字3.我们注意到由于此前累加的和是-1,小于0,那如果用-1加上3,得到的和是2,比3本身还小。也就是说,从第一个数字开始的子数组的和会小于从第三个数字开始的子数组的和。因此我们不用考虑从第一个数字开始的子数组,之前累加的和也被抛弃。
2、示例数组计算子数组的最大和的过程

步骤操作累加的子数组和最大的子数组和
1加111
2加-2-11
3抛弃前面的和-1,加333
4加101313
5加-4913
6加71616
7加21818
8加-51318
//使用一个全局变量来标记输入是否有效
bool g_InvalidInput = false;//false表示合格
//最大子数组的和
int FindGreatestSumOfArray(int* pData, int nLength)
{
    if (pData == nullptr || nLength <= 0)//不合格输入
    {
        g_InvalidInput = true;
        return 0;
    }
    g_InvalidInput = false;
    int nCurrentSum = 0;
    int nGreatestSum = 0x80000000;//?怎么回事???
    for (int i = 0; i < nLength; i++) {
        if (nCurrentSum <= 0)//只要是小于0就抛弃之前的累加
            nCurrentSum = pData[i];
        else
            nCurrentSum += pData[i];

        if (nCurrentSum > nGreatestSum)
            nGreatestSum = nCurrentSum;
    }
    return nGreatestSum;
}

解法2:应用动态规划

4、如果用函数 f(i) f ( i ) 表示以第i个数字结尾的子数组的最大和,那么我们需要计算出 max[f(i)] m a x [ f ( i ) ] ,其中 0i<n 0 ≤ i < n 。可以使用递归公式求 f(i) f ( i ) :
f(i)=pData[i],i=0f(i1)0 f ( i ) = p D a t a [ i ] , i = 0 或 者 f ( i − 1 ) ≤ 0
f(i)=f(i1)+pData[i],i0f(i1)>0 f ( i ) = f ( i − 1 ) + p D a t a [ i ] , i ≠ 0 或 者 f ( i − 1 ) > 0
5、当以第i-1个数字结尾的子数组中所有数字的和小于0时,如果把这个负数与第i个数累加,则得到的结果比第i个数字本身还要小,这种情况下以第i个数字结尾的子数组就是第i个数字本身。如果第i-1个数字结尾的子数组中所有数字的和大于0,则与第i个数字累加就得到以第i个数字结尾的子数组中所有数字的和
6、解法2的代码与解法1一样。 f(i) f ( i ) 就是nCurrentSum, max[f(i)] m a x [ f ( i ) ] 即使nGreatestSum
7、这道题要求的时间复杂度是O(n),对于上面的解法,都是只遍历一次数组即可,但是多了用于记录的变量,因为只遍历一次数组,所以时间复杂度是O(n)

1~n整数中1出现的次数

输入一个整数n,求1~n这n个整数的十进制表示中1出现的次数。例如,输入12,1~12这些整数中包含1的数字有1、10、11、12,1一共出现了5次。

思路

不考虑时间效率的解法,时间复杂度O(n logn)

1、通过对10求余判断整数的个位数字是不是1.如果这个数字大于10,则除以10之后再判断个位数字是不是1

int NumberOf1(unsigned int i)
{
    int number = 0;
    while (i)
    {
        if (i % 10 == 1)
            number++;
        i /= 10;
    }
    return number;
}

int NumberOf1Between1AndN(unsigned int n)
{
    int number = 0;
    for (unsigned int i = 1; i <= n; i++)
        number += NumberOf1(i);
    return number;
}

从数字规律着手明显提高时间效率的解法

2、如果希望不用计算每个数字的1的个数,那就只能去寻找1再数字中出现的规律了。
3、现在用一个较大的数21345作为例子来找到规律。把1~21345的所有数字分为两段:一段是1~1345;一段是1346~21345
4、先看1346~21345中1出现的次数。1的出现分两种情况。首先分析1出现再10000~19999这10000个数字的万位中,一共出现了10000次。
5、值得注意的是,并不是对所有5位数而言在万位出现的次数都是10000次。对于万位是1的数字如输入12345,1只出现在10000~12345的万位,出现的次数不是10000而是2346,也就是除去了最高位数字之后剩下的数字再加上1
6、接下来分析1出现在除最高位之外的其他4位数中的情况。例子中1346~21345这20000个数字中后4位中1出现的次数是8000次。由于最高位是2,我们再把1346~21345分为两段:1346~11345和11346~21345。每一段剩下的4位数字中,选择其中一位是1,其余三位可以在0~9.
2~5是书上写的思路,到这里已经卡到不行了!没能弄明白作者的思路,所以就求助度娘了。以下是参考的思路
以下参考:https://blog.csdn.net/yi_afly/article/details/52012593#commentsedit

个位

《【数据结构与算法(十九)】》
7、从1~n,每增加1,weight就会加1,当weight加到9时,再加1个位上的数又会回到0开始。那么weight从0~9的这种周期会出现多少次(也就是说个位上出现1的次数与n的关系是什么样的?)
8、以534为例,从1~529的过程中,个位从0~9变化了53次(0~52),刚好为round。每一轮变化,1出现1次,所以从1~529一共出现了53次1。
9、接着看看weight的值。weight为4,大于0,说明但round为53时,1又出现了1次。把1出现的次数记为count,所以 count=round+1=54 c o u n t = r o u n d + 1 = 54
那么如果weight的值为0,也就是说刚好就是530,那么就没有最后一共1出现了(531),也就是说 count=round=53 c o u n t = r o u n d = 53

十位

《【数据结构与算法(十九)】》
10、对于10位来说,其0~9周期的出现次数与个位的统计方式是相同的。不同点在于:从1~534,每增加10,十位的weight才会增加1,也就是说round对于weight的影响是round变一次,次数可以加10,那么round从0~4,一共变了5次。
11、最后一次,就是当round为5的时候,也就说51*能有多少个,与个位上的值有关,因为个位上是4,所以51*=510、511、512、513、514共5个,也就是个位4+1=5
12、总结【10和11】,十位上1出现的次数count=round(高位上的值)*10+地位上的值+1=5*10+4+1=55

其他高位

13、更高位的计算方式与十位上一致

总结

14、将n的各个数位分为两类:个位以及除个位以外的其他高位
15、对于n个位上的数unit来说:
unit=0countunit=round u n i t = 0 , c o u n t − u n i t = r o u n d
unit>0countunit=round+1 u n i t > 0 , c o u n t − u n i t = r o u n d + 1
16、对于高位来说,记每一位的权值为base,当前位为weight,低位为former,高位所有为round
《【数据结构与算法(十九)】》
①weight=0,则十位上1出现的次数 countdecade=roundbase c o u n t − d e c a d e = r o u n d ∗ b a s e
②weight=1,则十位上1出现的次数 countdecade=roundbase+former c o u n t − d e c a d e = r o u n d ∗ b a s e + f o r m e r
③weight>1,则十位上1出现的次数 countdecade=round+1base c o u n t − d e c a d e = ( r o u n d + 1 ) ∗ b a s e

代码
//时间复杂度O(n)
int NumberOf1Between1AndN_2(unsigned int n)
{
    int count = 0;
    int base = 1;
    int round = n;
    while (round > 0) {
        int current = round % 10;
        round /= 10;
        count += round * base;
        //如果对于n当前位正好是1,那该位上的1的个数就与低位有关系了
        if (current == 1)
            count += (n%base) + 1;//n%base是低位上的数
        else if (current > 1)//如果大于1,那就是说低位上的数可以走个遍
            count += base;
        base *= 10;
    }
    return count;
}
点赞