数组算法--分析思路

1. 一个数组中有若干整数,其中只有一个数只出现奇数次,其他数都出现偶数次,找出现奇数次的数。O(n) 

     解题思路:O(n) 循环遍历数组每个元素,执行异或操作,最后结果就是出现奇数次元素,原理:相同为零,不同为自己

2. 统计一个字符串中出现次数最多的字符

     解题思路:count[a[i]]++;最后遍历count

3. 求一个无序数组中的第K大的元素/K个最大元素

查找出一给定数组中第k大的数。例如[3,2,7,1,8,9,6,5,4],第1大的数是9,第2大的数是8…… 
思路一. 直接从大到小排序,排好序后,第k大的数就是arr[k-1]。 
思路二. 只需找到第k大的数,不必把所有的数排好序。我们借助快速排序中partition过程
,一般情况下,在把所有数都排好序前,就可以找到第k大的数。我们依据的逻辑是,经过一次partition后,数组被pivot分成左右两部分:S左、S右。

3.1 当 S左元素个数 |S左|等于k-1时,pivot即是所找的数;

3.2 当 S左元素个数 < k-1,所找的数位于S右中;

3.3 当 S左元素个数 > k-1,所找的数位于S左中。显然,后两种情况都会使搜索空间缩小。

int Partition(int *arr, int from, int to)
{
    int i, j;
    i = j = from;
    while (j <= to)
    {
        if (arr[j] >= arr[to]) {
            int tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
            i++;
        }
        j++;
    }
    return i - 1;
}
int quickSelect(int *s, int k, int left, int right)
{
    if (left == right) return s[left];
    int i;
    i = Partition(s, left, right);
    if (i - left + 1 == k) {
        return s[i];
    }
    else if (i - left + 1 < k) {
        return quickSelect(s, k - (i - left + 1), i + 1, right);
    }
    else return quickSelect(s, k, left, i - 1);
}
int findKthLargest(int* nums, int numsSize, int k) {
    return quickSelect(nums, k, 0, numsSize - 1);
}

思路三:二分搜索法:直接划定中值区间,通过比较的方法,找到第K个数所在的数值区间,然后二分搜索递归下去

寻找 N 个数中最大的 K 个数,本质上就是寻找最大的 K 个数中最小的那个,也就是第 K 大的数。可以使用二分搜索的策略来寻找 N 个数中的第 K 大的数。

思路四:堆排序法:非常适合内存有限,数据海量的情况

如果N数量非常大,而K个数可以在内存中加载,使用容量为K的小顶堆存储需要的K个数,剩下N-K个数,逐个存入小顶堆内,每次从N-K中读取一个数,如果这个数大于小顶堆的堆顶X,则替换X,重新调整小顶堆,小顶堆原理:所有元素必须大于堆顶元素,如果新加入堆顶元素,大于左右子元素,则需要重新调整堆结构,每次调整时间复杂度O(log2K)。如果小于小顶堆的堆顶元素X,则继续读取N-K中下一个元素。

算法只需要扫描所有的数据一次,时间复杂度为 O(N * log2K)

至于”查找数组中第k小的数“,那自然可以举一反三,同样处理了。


4. 一个N个整数的无序数组,给你一个数sum,求出数组中是否存在两个数,使他们的和为sum O(nlogn)

    解题思路:如果数组是无序的,先排序(NlogN),然后用两个指针i,j,各自指向数组的首尾两端,令i=0,j=n-1,然后i++,j--,逐次判断a[i]+a[j]?=sum,

  • 如果某一刻a[i]+a[j] > sum,则要想办法让sum的值减小,所以此刻i不动,j--;
  • 如果某一刻a[i]+a[j] < sum,则要想办法让sum的值增大,所以此刻i++,j不动。

所以,数组无序的时候,时间复杂度最终为O(N log N + N)=O(N log N)。

如果原数组是有序的,则不需要事先的排序,直接用两指针分别从头和尾向中间扫描,O(N)搞定,且空间复杂度还是O(1)。

通用思想

  • 先对数组排序

  • 对于两数,遍历数组,先确定一个数,再从剩下的元素中,从后往前遍历,找到与之之和等于目标值的数

  • 对于三数之和为零,遍历数组,先确定一个数,再从剩下的元素中,找出两个数,使这三数之和为零,找到这两个数的方法, 参考上一条。

  • 对于四数之和为零,遍历数组,先确定一个数,再从剩下的元素中,找出三个数,使这四数之和为零,找到这三个数的方法, 参考上一条。

  • 对于三数之和最接近目标值,与三数之和类似。不同的是,在每次遍历求和时,记录最接近的值。

5. 判断一个链表是否有环,即某个节点的next值为它前面的某个节点的地址n为节点个数

    解题思路:两个指针 一个逐个前进 一个蹦着前进, 判断是否相遇

bool HasCircle(ListNodeEx * pHead) {
    ListNodeEx * pFast = pHead; // 快指针每次前进两步
    ListNodeEx * pSlow = pHead; // 慢指针每次前进一步
    while(pFast != NULL && pFast->next != NULL) {
        pFast = pFast->next->next;
        pSlow = pSlow->next;
        if(pSlow == pFast) // 相遇,存在环
            return true;
    }
    return false;
}

6. 判断两个链表是否相交(假设无环) 如果相交,找第一个交点

    解题思路:相交的两个链表,一个尾巴,第二问 需要使用两个栈,保存指针,两个链表各自入栈,然后从尾部开始出栈

7. 如何确定一个未知长度的单链表的中间点

    解题思路:两个指针,一个逐步后移,一个每次后移两个,当后者到尾时,前者位于中间, O(n) j

//查找单链表的中间结点
ListNodeEx* GetMiddleNode(ListNodeEx* phead) {
    if (!phead || !phead->next) return NULL;

    ListNodeEx* pQuick = phead;
    ListNodeEx* pSlow  = phead;

    while (pQuick->next) {
        pQuick = pQuick->next;
        pSlow  = pSlow->next;
        if (pQuick->next)
            pQuick = pQuick->next;
    }
    return pSlow;
    return NULL;
}

8. 从一个无序数组中找出最长递减子序列,所谓子序列,其中各元素在原序列中可以不相邻 O(n^2)

    解题思路:动态规划,flag[j]代表以j为终点,且包含j在内的最长递减子序列长度 flag[j] = max(flag[i])+1 (i<j 且 a[j] < a[i]) 即在j前面的各个数字中,依次尝试将j放在其递减队列中 当满足括号内条件时,即可以放进,所有可以放进的可能中,选择使flag[j]最大的  即为flag[j]的值

9. 最大子段和 O(n) 

     解题思路: 动态规划,b[j]代表以j为终点的,且包括j在内的最大连续子段和,则b[j]= max{b[j-1]+a[j], a[j]},过程中记录最大的b[j],即为所求

10. 一个整数数组,要求在O(N)时间 O(1)空间内完成操作,奇数在前,偶数在后

     解题思路:数组前后两个指针,依次往中间靠拢,前指针遇到偶数,则覆盖后指针,后指针遇到奇数则覆盖前指针,需要实现保存第一个数

11. 一个字符串数组,找出其中只出现了一次的字符  

     解题思路:hash 分配一个26的数组,  此题有多个衍生,如果不是字符串数组 而是其他数据,则需要分配bit数组

12. 查找二叉树所有从根到叶子路径权值和为固定值的路径

    解题思路:递归,用一个栈保存路径

13. 反转一个链表

     解题思路:递归

//递归方式:实现单链表反转
ListNodeEx* Recrusive_ReverseList(ListNodeEx* head) {
    if (head == NULL || head->next == NULL) {
        return head;
    }
//    std::cout << "val=" << head->val << std::endl;
    ListNodeEx* newHead = Recrusive_ReverseList(head->next);// 倒数第二个节点 == > newHead
    head->next->next = head;//将后一个链表结点指向前一个结点
    head->next = NULL;//将原链表中前一个结点指向后一个结点的指向关系断开
    return newHead;
}

14. 反转一个字符串 2 反转一个句子,但是词不能反转了

   解题思路,反转字符串可以用前后两个指针交换  然后靠近  2 反转整个句子,然后挨个反转每个词

13. 复制一个复杂链表,复杂链表指的是,链表除了有一个next指针外,还有一个friend指针,指向链表中任意一个节点

  解题思路,

  思路1,先按照next指针遍历一遍,复制出一个仅包含next指针的链表,然后同时遍历新旧链表,同时各自保持两个指针,一个用来遍历节点,依次给每个节点的friend节点赋值,另一个用来定位friend节点 O(N^2)

  思路2 ,复制出来的节点先放在原节点的next,这样新节点的friend就是原节点的friend的next  O(N)

14. 从一个基本有序的数组中找到最小的数,基本有序,指的是数组是由一个有序的数组截断然后拼接而成  比如345 12

     解题思路:二分查找,如果找到的数比数组最后一数大,那么在后找,如果找到的数比数组最后一数小,在前找。

15. 原地归并排序

     解题思路:假设分界点为mid,两个指针分别指向两段中最小的,比如i前 j后,j=mid+1, j往后走,一直到a[j]>a[i],然后交换a[i,mid]和a[mid+1,j-1], 交换的算法,参考14题

16. 求无序数组的元素间最大差值

     解题思路:最简朴的方式是二层遍历,O(n^2), 方法2是两个数组 分别记录

17.  给出K个排序好的数组,用什么方法可以最快的把他们合并成为一个排序数组?

     解题思路:这中题目分布式系统经常运用到,比如来自不同客户端的排序好的链表想要在主服务器上面合并起来。

一般这种题目有两种做法。

第一种做法比较容易想到,就是有点类似于MergeSort的思路,就是分治法。先把k个list分成两半,然后继续划分,知道剩下两个list就合并起来,合并时会用到类似 Merge Two Sorted Lists 这道题的思路。 这种思路我们分析一下复杂度,如果有k个list,每个list最大长度是n,那么我们就有分治思路的复杂度计算公式 T(k) = 2T(k/2)+O(n*k)。 其中T(k)表示k个list合并的时间复杂度,用主定理可以算出时间复杂度是O(nklogk)。

第二种做法是运用堆,也就是我们所说的priority queue。我们可以考虑维护一个大小为k的堆,先把每个list的第一个元素放入堆之中,然后每次从堆顶选取最小元素放入结果最后的list里面,然后读取该元素所在list的下一个元素放入堆中,重新维护好堆,然后重复这个过程。因为每个链表是有序的,每次又是取当前k个元素中最小的,所以最后结果的list的元素是按从小到大顺序排列的。这种方法的时间复杂度也是O(nklogk)。

18. 寻找前K大个数

方法1: 创建长度为K小顶堆,不够K个读入K个数据

剩下然后读入N-K个数,每次读入一个数就与当前的根进行比较,如果大于当前根,则替换之,并调整堆。如果小,则读入下一个。

时间复杂度O(N*logK)

方法2:利用快排分区思想:

本题还有一个时间复杂度比较好的做法。在编程之美上提到过该算法。

首先找到最大的第K个数。这个时间复杂度可以做到O(N),具体做法如下(利用快排分区思想):

从N个数中随机选择一个数,扫描一遍,比n大的放在右边,r个元素,比n小的放左边,l个元素

如果:  a:l = K-1  返回n

             b:l > K-1 在l个元素中继续执行前面的操作。

             c:l < K-1  在r个元素中继续执行前面的操作。

b c每次只需执行一项,因此平均复杂度大概为:O(n+n/2+n/4...)=O(2n)=O(n)

19.  K路合并求Top-K

有 20 个数组,每个数组有 500 个元素,并且是有序排列好的,现在在这 20*500个数中找出排名前 500 的数。

方法1:从20个数组中各取一个数,并记录每个数的来源数组,建立一个含20个元素的大根堆。此时堆顶就是最大的数,取出堆顶元素,并从堆顶元素的来源数组中取下一个数加入堆,再取最大值,一直这样进行500次即可。

20.   K路合并排序

请给出一个时间为O(nlgk)、用来将k个已排序链表合并为一个排序链表的算法,此处n为所有输入链表中元素的总数。

解法一:

1. 从k个链表中取出每个链表的第一个元素,组成一个大小为k的数组arr,然后将数组arr转换为最小堆,那么arr[0]就为最小元素了;

2. 取出arr[0],将其放到新的链表中,然后将arr[0]元素在原链表中的下一个元素补到arr[0]处,即arr[0].next,如果 arr[0].next为空,即它所在的链表的元素已经取完了,那么将堆的最后一个元素补到arr[0]处,堆的大小自动减1,循环即可。

21.  整体有序局部无序问题

一个有100亿个元素的整型数组,它的元素是有序的,现在把它分成若干段,每段不超过20个元素,每段的元素个数不等,现在在每段内将这些元素的顺序打乱,然后重新将这100亿个元素的数组排序,请问时间复杂度最小的算法是什么?并给出时间复杂度。

解法一:(直接插入排序)

假设第1到第5n个数已经有序为sort(5n),那么我们要将5n+1到5n+5这5个数据添加到已排序的数组中,只需要进行插入排序,将这5个数添加进即可。由于分段的长度不超过5,所以第5n+1个数在插入的时候,最多只需要搜索到第5n-4个数就可以了,比较个数不会超过5次。又因为5n+1到5n+5是已经排好序的,所以,后面的数比较次数也不会超过5次(最多比较到前一个插入的位置)。因此,每加入5个数到已排序数组中,时间复杂度是O(5*5),

假设长度为N,每段长不超过K。则每段插入的时间复杂度即为O(K*K)。

而对于以段为单位插入的操作,需要进行N/K次,所以,总的时间复杂度是O(K*K)*O(N/K)=O(NK)

 

上述算法的可选实现工具:自己建立数组进行堆排序、使用优先队列priority_queue、使用集合set multiset
在最大堆中,根结点的值总是大于它的子树中任意结点的值。于是我们每次可以在O(1)得到已有的k个数字中的最大值18,但需要O(logk)时间完成删除以及插入操作。
队列(queue)维护了一组对象,进入队列的对象被放置在尾部,下一个被取出的元素则取自队列的首部。

priority_queue特别之处在于,允许用户为队列中存储的元素设置优先级。这种队列不是直接将新元素放置在队列尾部,而是放在比它优先级低的元素前面。优先队列有两种,一种是最大优先队列;一种是最小优先队列;每次取自队列的第一个元素分别是优先级最大和优先级最小的元素。
在STL中set和multiset都是基于红黑树实现的,查找、删除和插入操作都只需要O(logk)。

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值