1. 一个数组中有若干整数,其中只有一个数只出现奇数次,其他数都出现偶数次,找出现奇数次的数。O(n)
解题思路:O(n) 循环遍历数组每个元素,执行异或操作,最后结果就是出现奇数次元素,原理:相同为零,不同为自己
解题思路:count[a[i]]++;最后遍历count
查找出一给定数组中第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. 判断两个链表是否相交(假设无环) 如果相交,找第一个交点
解题思路:相交的两个链表,一个尾巴,第二问 需要使用两个栈,保存指针,两个链表各自入栈,然后从尾部开始出栈
解题思路:两个指针,一个逐步后移,一个每次后移两个,当后者到尾时,前者位于中间, 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]的值
解题思路: 动态规划,b[j]代表以j为终点的,且包括j在内的最大连续子段和,则b[j]= max{b[j-1]+a[j], a[j]},过程中记录最大的b[j],即为所求
10. 一个整数数组,要求在O(N)时间 O(1)空间内完成操作,奇数在前,偶数在后
解题思路:数组前后两个指针,依次往中间靠拢,前指针遇到偶数,则覆盖后指针,后指针遇到奇数则覆盖前指针,需要实现保存第一个数
解题思路:hash 分配一个26的数组, 此题有多个衍生,如果不是字符串数组 而是其他数据,则需要分配bit数组
解题思路:递归,用一个栈保存路径
解题思路:递归
//递归方式:实现单链表反转
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;
}
解题思路,反转字符串可以用前后两个指针交换 然后靠近 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)。