LintCode解题记录17.4.28

本文记录了作者在LintCode上刷Heap相关题目的心得,包括LintCode 130 Heapify、401. kth smallest number in matrix、104 Merge k Sorted Lists等题目。通过讲解各种解题策略,如使用min_heap、max_heap和维护堆的技巧,深入探讨了Heap在解决实际问题中的应用,并强调了理解和实践堆的重要性。
摘要由CSDN通过智能技术生成

今天主要刷和Heap相关的几道题。
LintCode 130 Heapify
给一个vector,根据此来建立min_heap。重要,需要掌握!
注意一个细节:
若数组编号从1开始,则对于编号为i的节点来说,其左孩子编号为2i,右孩子编号为2i+1,父亲节点编号为i/2。
若数组编号从0开始,则左孩子编号为2i+1,右孩子编号为2i+2,父亲节点编号为(i-1)/2。

class Solution {
public:
    /**
     * @param A: Given an integer array
     * @return: void
     */
    void Sift(vector<int> &R, int low, int high)
    {
        int temp = R[low];
        int i = low, j = 2*i + 1;
        while (j <= high)
        {
            if (j < high && R[j] > R[j+1]) j++;//right is smaller than left.
            if (R[j] < temp) //if child is smaller than parent.
            {
                R[i] = R[j];
                i = j;
                j = 2*i + 1;
            }
            else
                break;
        }
        R[i] = temp;
    }
    void heapify(vector<int> &A) {
        // write your code here
        //heapify it into a min-heap array
        for (int i = A.size()/2-1; i >= 0; i--)
        {
            Sift(A, i, A.size()-1);
        }
    }
};

LintCode 401. kth smallest number in matrix
经典的topK问题。可以用partition以O(N)的复杂度解决,结合堆的大背景,这里给出两种思路。
这里用了STL中的优先队列priority_queue。看到这里请先白板实现一下priority_queue的push和pop操作。
1.遍历一遍矩阵,把所有的值push进优先队列。然后一次pop出k-1个值之后,队列的top元素就是返回值。这里的声明的堆为min_heap。复杂度为O(N+(k-1)*logN) = O(klogN)。
2.维护一个大小为k的max_heap,首先压入k个数建堆。接着遍历到下一个数时,如果该数小于top元素,则删除top元素之后再将其push进去,重复该过程直到遍历结束,那么此时的top元素就是返回值。复杂度为O(k+(N-k)logk) = O(Nlogk)。
第一种方法看起来直观一点,第二种方法需要拐一个弯。

    int kthSmallest(vector<vector<int> > &matrix, int k) {
        // write your code here
        //seem like it can use priority_queue
        priority_queue<int, vector<int>, greater<int> > pq;
        for (int i = 0; i < matrix.size(); i++)
        {
            for (int j = 0; j < matrix[i].size(); j++)
            {
                pq.push(matrix[i][j]);
            }
        }
        int total = pq.size();
        while (pq.size() > total-k+1) pq.pop();
        return pq.top();
    }

    int kthSmallest(vector<vector<int> > &matrix, int k) {
        // write your code here
        //seem like it can use priority_queue
        priority_queue<int> pq;
        for (int i = 0; i < matrix.size(); i++)
        {
            for (int j = 0; j < matrix[i].size(); j++)
            {
                if (pq.size() < k) pq.push(matrix[i][j]);
                else
                {
                    if (pq.top() > matrix[i][j])
                    {
                        pq.pop();
                        pq.push(matrix[i][j]);
                    }
                }
            }
        }
        return pq.top();
    }
};

LintCode 104 Merge k Sorted Lists (这题很重要,重点理解)
这道题目在分布式系统中非常常见,来自不同client的sorted list想要在central server上merge起来。
1.暴力解决。把所有不为NULL的节点的值压到优先队列里,然后在利用尾插法建立链表。最后返回第一个节点。时间复杂度为O(kn+kn*logkn)。
2.第一种方法太蠢了,竟然也能AC。利用Divide_and_Conquer的思想,把k个有序链表不断二分,直到分为只有一个或者只有两个链表,然后再用merge two sorted lits的方法(复杂度为线性O(n)),那么最终复杂度为O(nlogn),具体到本体来说复杂度就是O(nklogk)。具体来说是用了合并排序的思想。注意一下最后的merge two sorted lists的实现。

    ListNode *mergeTwoLists(ListNode *listA, ListNode *listB)
    {
        ListNode *p = new ListNode;
        ListNode *q = p; //先要把初始节点保存起来,然后再去操作其中一个,最后返回初始的节点
        while (NULL != listA && NULL != listB)
        {
            if (listA->val < listB->val)
            {
                p->next = listA;
                p = listA;
                listA = listA->next;
            }
            else
            {
                p->next = listB;
                p = listB;
                listB = listB->next;
            }
        }
        if (NULL != listA)
        {
            p->next = listA;
        }
        if (NULL != listB)
        {
            p->next = listB;
        }
        return q->next;
    }

    ListNode *merge(vector<ListNode*> &lists, int left, int right)
    {
        if (left == right) return lists[left];
        int mid = (left+right)/2;
        ListNode *listA = merge(lists, left, mid);
        ListNode *listB = merge(lists, mid+1, right);
        return mergeTwoLists(listA, listB);
    }

    ListNode *mergeKLists(vector<ListNode *> &lists) {
        // write your code here
        if (lists.size() == 0) return NULL;
        return merge(lists, 0, lists.size()-1);
    }

3.第三种方法就符合今天的主题了——堆。即维护一个大小为k的最小堆,每次弹出堆顶存到一个链表中,然后把堆顶元素的next指针压入到堆中,重新调整堆。重复上述操作直到堆为空。由于这些链表都是有序的,所以可以保证最后的链表是有序的。假设每个链表有n个元素,那么一共有nk个元素,把一个元素压入大小为k的堆的复杂度为O(logk),所以总的复杂度为O(nklogk+nklogk) = O(nklogk)。
这里注意一下priority_queue第三个参数cmp的写法。

    struct cmp
    {
        bool operator () (const ListNode* a, const ListNode* b)
        {
            return a->val > b->val;
        }
    };
    ListNode *mergeKLists(vector<ListNode *> &lists) {
        // write your code here
        priority_queue<ListNode*, vector<ListNode*>, cmp> pq;
        for (int i = 0; i < lists.size(); i++)
        {
            if (NULL != lists[i]) pq.push(lists[i]);
        }
        ListNode *p, *q;
        p = new ListNode;
        q = p;
        while (!pq.empty())
        {
            p->next = pq.top();
            pq.pop();
            p = p->next;
            if (p->next)
                pq.push(p->next);
        }
        return q->next;
    }

4.当然还有一种解法,就是12合并,结果赋给2,然后23合并,一直到最后一个元素。遍历12需要遍历2n个点,遍历123需要3n个点,一直到最后需要遍历kn个点。那么总遍历点的数目就是n(2+3+..+k)=n(k+2)*(k-1)/2,所以复杂度为O(nk2)。代码就不贴了。

LintCode 518 Super Ugly Numbres(Medium)
这道题是之前Ugly Number和Ugly NumberII的拓展,总体思路上来说和II没什么区别。首先很容易想到,从1开始一个一个判断,知道找到第n个丑数。但是要知道其实大部分数都不是丑数,所以这样算肯定很浪费时间(事实证明也确实超时了),所以要把目光放在丑数上。
可以发现,产生新的一个丑数 = 一个比其小的丑数x素因子,而这个比其小的丑数来自与已经求出的丑数。实际上对于每一个不同的素因子prime[i],都可以对应产生出一个有序链表,即 ugly[idx]*prime[i] -> ugly[idx+1]*prime[i]->….
所以实际上n个素因子就对应着n条链表,每次从该链表的头结点中取出最小的一个存到最终的答案里,然后该条链表的头结点就向后移动一位。需要注意有可能存在头结点中有相同的最小值,所以要把这些相同的最小值的头结点全部向后移动。所以这题和heap的有什么紧密的联系吗????

    int nthSuperUglyNumber(int n, vector<int>& primes) {
        // Write your code here
        int len = primes.size();
        vector<int> ugly(1, 1);
        vector<int> idx(len, 0);
        while (ugly.size() < n)
        {
            int temp = 0x7fffffff;
            for (int i = 0; i < len; i++)
            {
                temp = min(temp, ugly[idx[i]] * primes[i]);
            }
            for (int i = 0; i < len; i++)
            {
                if (temp == ugly[idx[i]] * primes[i])
                    idx[i]++;
            }
            ugly.push_back(temp);
        }
        return ugly[n - 1];
    }

LintCode 81 Data Stream Median 此题重要,需回顾掌握。
给定一个输入序列,求每输入一个数时的中位数。要求时间复杂度是O(nlogn)
首先是求中位数问题:可以用快速选择来实现一个O(N)的算法,但是若每输入一个数,都这样操作,那么复杂度就是O(n^2)。事实证明这种算法确实超时了。不过这种线性时间求中位数的代码需要掌握。
本题是在堆这个大背景下,用一个大堆和一个小堆来解决问题。大堆存储左边的数,小堆存储右边的数,只要大堆的堆顶 < 小堆的堆顶,就可以保证大堆里的全部数小于小堆。那么,只要保证当n是偶数时,大堆的大小等于小堆的大小,当n是奇数时,大堆的大小等于小堆的大小加1就能保证 大堆的堆顶就是我们想要的中位数。

    priority_queue<int> max_pq;//stl中的优先队列,默认是大堆
    priority_queue<int, vector<int>, greater<int> > min_pq;//声明成小堆
    vector<int> medianII(vector<int> &nums) {
        // write your code here
        vector<int> ans;
        if (nums.size() == 0) return ans;
        max_pq.push(nums[0]);
        ans.push_back(nums[0]);
        //首先把第一个数添加进去
        for (int i = 1; i < nums.size(); i++){
            int top = max_pq.top();
            //添加一个数,如果该数大于大堆堆顶,那么就加入右半部分的小堆中,否则就加入左半部分的大堆中
            if (nums[i] > top) min_pq.push(nums[i]);
            else max_pq.push(nums[i]);

            if (max_pq.size() > min_pq.size()+1){
            //如果大堆的大小大于小堆的大小加1,那么说明真正的中位数应该在大堆里。
            //弹出大堆堆顶,把此数压入小堆中
                int tmp = max_pq.top();
                max_pq.pop();
                min_pq.push(tmp);
            }else if (max_pq.size() < min_pq.size()){
            //反之同理
                int tmp = min_pq.top();
                min_pq.pop();
                max_pq.push(tmp);
            }
            //最后大堆堆顶就是中位数
            ans.push_back(max_pq.top());
        }
        return ans;
    }

总结:
1.刷题是很快,但是去学习别人的做法,再查书加深记忆一下相关知识点,最后再结合自己的理解写出来往往要花费比做这道题还要多的时间。所以不能太浮躁,一味的求快。
2.做这些题时,顺带又去看了一下Heap的相关实现,自己又理解了一下,包括对复杂度的理解。只能说对堆的理解更加深了一步吧,但光说不练,花架势。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值