算法刷题:题库 - 力扣 (LeetCode) 全球极客挚爱的技术成长平台
1.把文件中的一组整数排序后输出到另一个文件中
考点:运用vector容器解决实际问题
这个题目牵涉到文件操作以及排序。我们可以使用vector容器来简化文件操作。在读文件的时候用push_back把所有的整数放入一个vector<int>对象中,在写文件时用[]操作符直接把vector<int>对象输出到文件。代码如下:
#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
using namespace std;
//对data容器中的所有元素进行冒泡排序
void Order(vector<int>& data)
{
int tag = false ; // 设置是否需要继续冒泡的标志位
int count = data.size(); //获得vector中的元素个数
for (int i=0 ; i<count ; i++)
{
for (int j=0; j<count-i-1; j++)
{
if (data[j] > data[j+1]) //如果当前元素比下一个元素大,则交换
{//结果为升序排列
tag = true ;
std::swap(data[j], data[j+1]);
}
}
if ( !tag )
{
break ;
}
}
}
int main( void )
{
vector<int> data;
ifstream in("c:\\data.txt");
if (!in) //打开输出文件失败
{
cout<< "infile error!" << endl;
return 1;
}
int temp;
while (!in.eof())
{
in >> temp; //从文件中读取整数
data.push_back(temp); //把读取的证书放入data容器中
}
in.close();
Order(data); //冒泡排序
ofstream out("c:\\result.txt");
if (!out) //打开输出文件失败
{
cout<<"outfile error!" << endl;
return 1;
}
for (int i = 0 ; i < data.size() ; i++)
{
out << data[i] << " "; //把data容器中的所有元素输出至文件
out.close();
return 0;
}
}
2.已知单向链表的头结点head,写一个函数把这个链表逆序
我们假设单向链表的节点如下:
typedef struct LIST_NODE
{
int data;
LIST_NODE *next;
}LIST_NODE;
方法一:迭代循环,不使用额外的节点存储空间
LINK_NODE *ReverseLink(LINK_NODE *head)
{
LINK_NODE *next;
LINK_NODE *prev = nullptr; //循环的初始条件是:prev = nullptr;
while (head != nullptr) //循环终止条件是:head == nullptr
{
next = head->next; //保存下一个节点
head->next = prev; //当前的下一个节点变成前节点
prev = head; //当前节点变成前节点
head = next; //头指针往前移
}
return prev; //最后的前节点就是逆序后的头节点
}
方法二:递归,终止条件就是链表只剩一个节点时直接返回这个节点的指针
LINK_NODE *ReverseLink(LINK_NODE *head)
{
LINK_NODE *newHead;
if((head == nullptr) || (head->next == nullptr))
return head;
newHead = ReverseLink(head->next); /*递归部分*/
head->next->next = head; /*核心:回朔部分*/
head->next = nullptr;
return newHead;
}
对于线性数据结构,比较适合用迭代循环方法,而对于树状数据结构,比如二叉树,递归方法则非常简洁优雅。
(2) 已知两个链表 head1 和 head2 各自有序,请把它们合并成一个链表依然有序。
( 保留所有结点,即便大小相同)
Node * Merge(Node *head1 , Node *head2)
{
if (head1 == nullptr)
return head2;
if (head2 == nullptr)
return head1;
Node *head = nullptr;
Node *p1 = nullptr;
Node *p2 = nullptr;
if (head1->data < head2->data)
{
head = head1;
p1 = head1->next;
p2 = head2;
}
else
{
head = head2;
p2 = head2->next;
p1 = head1;
}
Node *pCurrent = head;
while (p1 != nullptr && p2 != nullptr)
{
if (p1->data <= p2->data)
{
pCurrent->next = p1;
pCurrent = p1;
p1 = p1->next;
}
else
{
pCurrent->next = p2;
pCurrent = p2;
p2 = p2->next;
}
}
if (p1 != nullptr)
pCurrent->next = p1;
if (p2 != nullptr)
pCurrent->next = p2;
return head;
}
(3) 已知两个链表 head1 和 head2 各自有序,请把它们合并成一个链表依然有序,
这次要求用递归方法进行。
Node * MergeRecursive(Node *head1 , Node *head2)
{
if (head1 == nullptr)
return head2;
if (head2 == nullptr)
return head1;
Node *head = nullptr;
if (head1->data < head2->data)
{
head = head1;
head->next = MergeRecursive(head1->next, head2);
}
else
{
head = head2;
head->next = MergeRecursive(head1, head2->next);
}
return head;
}
3.写一个函数找出一个整数数组中,第二大的数
const int MINNUMBER = -32767;
int find_sec_max( int data[] , int count)
{
int maxnumber = data[0]; //初始化第一大数
int sec_max = MINNUMBER; //初始化第二大数
for ( int i = 1 ; i < count ; i++)
{
if ( data[i] > maxnumber ) //是否大于最大数,修改第一大数和第二大数
{
sec_max = maxnumber;
maxnumber = data[i];
}
else
{
if ( data[i] > sec_max ) //是否大于第二大数
sec_max = data[i];
}
}
return sec_max;
}
4.如何判断一个单链表是否有环的
基本思路:用一个慢指针,一个快指针,慢指针一次走一个节点,快指针一次都两个节点,判断快指针是否能追上慢指针,当快指针追上慢指针时,说明有环,否则无环
bool check(const NODE* head)
{
if(head == nullptr)
return false;
NODE *low =head;
NODE *fast=head->next;
while (fast != nullptr && fast->next != nullptr)
{//快指针一次要走两个节点,如果无环且到最后一个,只用fast判断的话,走两个指针就段错误了
low = low->next;
fast = fast->next->next;
if(low == fast)
return true;
}
return false;
}
5.如果编写一个标准 strcpy 函数
char* strcpy(char *strDest, const char *strSrc) // 输入参数 const
{
assert( (strDest != nullptr) &&(strSrc != nullptr) ); //断言
char *address = strDest;
while( (*strDest++ = * strSrc++) != ‘/0’ );
return address; //支持链式操作
}
int strlen( const char *str ) // 输入参数 const
{
assert(str != nullptr); // 断言字符串地址非 0
int len=0; // 注,一定要初始化。
while( (*str++) != '/0' ) {
len++;
}
return len;
}
6.String 的具体实现
已知 String 类定义如下:
class String
{
public:
String(const char *str = nullptr); // 通用构造函数
String(const String &another); // 拷贝构造函数
~ String(); // 析构函数
String & operater =(const String &rhs); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
写出类的成员函数实现:
String::String(const char *str)
{
if (str == nullptr) //strlen 在参数为 nullptr 时会抛异常才会有这步判断
{
m_data = new char[1];
m_data[0] = '/0';
}
else
{
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
}
}
String::String(const String &another)
{
m_data = new char[strlen(another.m_data) + 1];
strcpy(m_data, other.m_data);
}
String& String::operator=(const String &rhs)
{
if (this == &rhs) //自拷贝
return *this;
delete []m_data; // 删除原来的数据,新开一块内存
m_data = new char[strlen(rhs.m_data) + 1];
strcpy(m_data,rhs.m_data);
return *this ;
}
String::~String()
{
delete []m_data ;
}
7.用递归算法判断数组 a[N] 是否为一个递增数组
递归的方法,记录当前最大的,并且判断当前的是否比这个还大,大则继续,否则返回 false 结束:
bool fun( int a[], int n )
{
if( n==1 )
return true;
if( n==2 )
return a[n-1] >= a[n-2];
return fun(a, n-1) && (a[n-1] >= a[n-2]);
}
8.判断字符串是否为回文
#include <stdio.h>
#include <string.h>
int main()
{
char a[100]= {0};
int i = 0;
int len = 0;
printf("please input character string:\n");
gets(a);
len = strlen(a); //计算输入字符串的长度;
for(i = 0; i < (len / 2); i++) //只需要判断前一半(len/2)长度就好了
{
if(a[i] != a[len - 1 - i]) //判断是否为回文数;
{
printf("不是回文数\n");
return 0;
}
}
printf("是回文数\n");
return 0;
}
8.字符串倒序
写一个函数将 "tom is cat" 倒序打印出来,即 "cat is tom"
思路1:按空格分隔单词,保存到std::vector<string>,最后逆序输出单词
思路2:逆序字符串,逐个单词判断,遇到空格,输出一个单词,需要记住每个单词的结尾和开头
9.请判断两个字符是否互为变形词
给定两个字符串str1与str2,如果两个字符串中出现的字符种类一样且每种字符出现的次数也一样,那么str1与str2互为变形词,
分析:使用哈希表分别记录每个字符出现的次数。也可以建立一个256的数组,代替哈希表。
bool simpleWord(char *str1, char *str2, const int &length) {
int num1[256] = { 0 };
int num2[256] = { 0 };
for (int i = 0; i < length; ++i) {
++num1[str1[i]];
}
for (int i = 0; i < length; ++i) {
++num2[str2[i]];
}
bool flag = true;
for (int i = 0; i < length; ++i) {
if (num1[i] != num2[i]) {
flag = false;
break;
}
}
return flag;
}
10.i代表str中的位置,将str[0…i]移到右侧,str[i+1…N-1]移到左侧
要求:时间复杂度为O(N) ,额外空间复杂度为O(1) 。
举例:str=“ABCDE”,i=2,将str调整为“CDEAB”
可以直接用substr!
分析:
1.首先将0 - i逆序
2.将i+1 - N-1逆序
3.将str整体逆序
#include<iostream>
#include<string>
#include<vector>
void reverseWord(std::string &str) { //字符串反向
for (int i = 0; i < str.length() / 2; ++i) {
auto temp = str[i];
str[i] = str[str.length() - i - 1];
str[str.length() - i - 1] = temp;
}
}
void reverseWord(std::string &str, const int &start,const int &end)
{ //字符串从start到end 反转
for (int i = 0; i < (end - start + 1) / 2; ++i) {
char temp = str[start + i];
str[start + i] = str[end - i];
str[end - i] = temp;
}
}
void translationWord(std::string &str, const int &position)
{ //将字符串0~position移到最右侧
reverseWord(str, 0, position); //"ABCDE"变"BACDE",position为1
reverseWord(str, position + 1, str.length() - 1); //"BACDE"变"BAEDC"
reverseWord(str); //"BAEDC"变"CDEAB"
}
11.给定一个字符串str,将其中所有空格字符替换成“%20”,假设str后面有足够的空间。
分析:
1.计算出空格数量,得到最终字符的长度。
2.从后往前写入字符。
#include<string>
void replaceSpace(std::string &str) {//将空格替换为“%20”
int spaceNum = 0;
int lenOld = str.length() - 1;
for (auto i : str) {
if (i == ' ') {
++spaceNum;
}
}
std::string addition(spaceNum * 2, ' ');
str = str + addition;
int lenNew = lenOld + 2 * spaceNum;
for (int i = lenOld; i >= 0; --i) {
if (str[i] != ' ') {
str[lenNew--] = str[i];
}
else if (str[i] == ' ') {
str[lenNew--] = '0';
str[lenNew--] = '2';
str[lenNew--] = '%';
}
}
}
12.给定一个字符串,其中只包含左括号或右括号,判断是不是整体有效的括号字符串。
时间复杂度为O(N) ,额外空间复杂度为O(1) 。
分析:使用num计算左右括号数,如果是左括号num++,如果是右括号num––,遍历的过程中如果出现num<0,则直接返回false,最终如果num==0,则返回true,否则为false。
13.给定一个字符串str,返回str的最长无重复字符子串的长度
蛮力法自然是两层for循环搞定,但是效率堪忧,需要想办法将复杂度调整到O(n),也就是只遍历一遍字符串,这就需要记录在之前是否有遇到某个字符,可以使用unordered_map,不过这里因为仅仅是字符,那么创建一个大小为256的vector就可以了,反而显得节省空间。
细想,每个不包括重复字符的子串肯定是一段连续的字符区域,头设定为front,尾设定为back。那么在仅仅遍历一遍的情况下,front肯定是不断改变了,back则就是当前遍历到的位置
如果当前遍历到的这个字符在front后面没有出现过,那么front不需要移动,接着遍历后面的字符
如果当前遍历到的这个字符在front后面出现过,那么从front到当前位置这个子串肯定就有重复的字符了,此时就需要改变front的位置到出现的那个字符后面的位置。也就是和当前遍历到的这个字符上一次出现的位置的下一个位置。
在这个过程中,时刻更新最大的长度,因为front到back这段区域永远不可能有重复的字符,如果有,已经在第二步解决了
class Solution {
public:
int lengthOfLongestSubstring(string s) {
/* 初始256大小,因为所有字符也就这么多,值记录着最后一次出现时在s中的下标,没有则是-1 */
vector<int> dp(256, -1);
int max_len = 0;
int front = 0;
for(int i = 0; i < s.size(); ++i)
{
/* 如果当前字符在front后面出现过一次 */
if(dp[s[i]] != -1 && dp[s[i]] >= front)
{
/* 改变front的位置,指向当前遍历到的字符上一次出现的位置的后面的位置 */
front = dp[s[i]] + 1;
}
/* 时刻更新每个字符最后一次出现时的位置 */
dp[s[i]] = i;
/* 时刻计算最大的长度,当前子串区域为[front, i] */
max_len = max(max_len, i - front + 1);
}
return max_len;
}
};
(2) 求最大不重复子串,在最后求长度地方修改下即可:
if (i - front + 1 > max_len) {
max_len = max(max_len, i - front + 1);
memcpy(maxSubString, &s[front], max_len);
}
14.搜索的输入信息是一个字符串,统计300万输入信息中的最热门的前10条,我们每次输入的一个字符串为不超过255byte,内存使用只有1G。请描述思想,写出算法(c语言),空间和时间复杂度。
答案:
300万个字符串最多(假设没有重复,都是最大长度)占用内存3M*1K/4=0.75G。所以可以将所有字符串都存放在内存中进行处理。
可以使用key为字符串,值为字符串出现次数的hash来统计每个每个字符串出现的次数。并用一个长度为10的数组/链表来存储目前出现次数最多的10个字符串。 这样空间和时间的复杂度都是O(n)。
寻找热门查询,300万个查询字符串中统计最热门的10个查询_moonboat0331的博客-CSDN博客_从 300 万字符串中找到最热门的 10 条
15.海量数据处理 - 10亿个数中找出最大的10000个数(top K问题)
海量数据处理 - 10亿个数中找出最大的10000个数(top K问题)_yofer张耀琦的博客-CSDN博客
16.最长回文子串(C语言)
LeetCode第五题:最长回文子串(C语言)_The Laughing Uncle的博客-CSDN博客_c语言最长回文子串
17.海量数据排序——如果有1TB的数据需要排序,但只有32GB的内存如何排序处理?
1、外排序
传统的排序算法一般指内排序算法,针对的是数据可以一次全部载入内存中的情况。但是面对海量数据,即数据不可能一次全部载入内存,需要用到外排序的方法。外排序采用分块的方法(分而治之),首先将数据分块,对块内数据按选择一种高效的内排序策略进行排序。然后采用归并排序的思想对于所有的块进行排序,得到所有数据的一个有序序列。
例如,考虑一个1G文件,可用内存100M的排序方法。首先将文件分成10个100M,并依次载入内存中进行排序,最后结果存入硬盘。得到的是10个分别排序的文件。接着从每个文件载入9M的数据到输入缓存区,输出缓存区大小为10M。对输入缓存区的数据进行归并排序,输出缓存区写满之后写在硬盘上,缓存区清空继续写接下来的数据。对于输入缓存区,当一个块的9M数据全部使用完,载入该块接下来的9M数据,一直到所有的9个块的所有数据都已经被载入到内存中被处理过。最后我们得到的是一个1G的排序好的存在硬盘上的文件。
2、1TB数据使用32GB内存如何排序
①、把磁盘上的1TB数据分割为40块(chunks),每份25GB。(注意,要留一些系统空间!)
②、顺序将每份25GB数据读入内存,使用quick sort算法排序。
③、把排序好的数据(也是25GB)存放回磁盘。
④、循环40次,现在,所有的40个块都已经各自排序了。(剩下的工作就是如何把它们合并排序!)
⑤、从40个块中分别读取25G/40=0.625G入内存(40 input buffers)。
⑥、执行40路合并,并将合并结果临时存储于2GB 基于内存的输出缓冲区中。当缓冲区写满2GB时,写入硬盘上最终文件,并清空输出缓冲区;当40个输入缓冲区中任何一个处理完毕时,写入该缓冲区所对应的块中的下一个0.625GB,直到全部处理完成。
3、继续优化
磁盘I/O通常是越少越好(最好完全没有),那么如何降低磁盘I/O操作呢?关键就在第5和第6步中的40路输入缓冲区,我们可以先做8路merge sort,把每8个块合并为1路,然后再做5-to-1的合并操作。
再深入思考一下,如果有多余的硬件,如何继续优化呢?有三个方向可以考虑:
使用并发:如多磁盘(并发I/O提高)、多线程、使用异步I/O、使用多台主机集群计算。
提升硬件性能:如更大内存、更高RPM的磁盘、升级为SSD、Flash、使用更多核的CPU。
提高软件性能:比如采用radix sort、压缩文件(提高I/O效率)等。
==================================黄金分割线==============================
1. 在 O(1) 时间删除链表节点
题目描述:给定单向链表的头指针和一个节点指针,定义一个函数在O(1)时间删除该节点。
解题思路:常规的做法是从链表的头结点开始遍历,找到需要删除的节点的前驱节点,把它的 next 指向要删除节点的下一个节点,平均时间复杂度为O(n),不满足题目要求。 那是不是一定要得到被删除的节点的前一个节点呢?其实不用的。我们可以很方面地得到要删除节点的下一个节点,如果我们把下一个节点的内容复制到要删除的节点上覆盖原有的内容,再把下一个节点删除,那就相当于把当前要删除的节点删除了。举个栗子,我们要删除的节点i,先把i的下一个节点j的内容复制到i,然后把i的指针指向节点j的下一个节点。此时再删除节点j,其效果刚好是把节点i给删除了。 要注意两种情况:
- 如果链表中只有一个节点,即头节点等于要删除的节点,此时我们在删除节点之后,还需要把链表的头节点设置为NULL。
- 如果要删除的节点位于链表的尾部,那么它就没有下一个节点,这时我们就要从链表的头节点开始,顺序遍历得到该节点的前序节点,并完成删除操作。
参考代码
ListNode* deleteNode(ListNode* head, ListNode* toBeDeleted) {
// 如果输入参数有空值就返回表头结点
if (head == nullptr || toBeDeleted == nullptr) {
return head;
}
// 如果删除的是头结点,直接返回头结点的下一个结点
if (head == toBeDeleted) {
return head->next;
}
// 下面的情况链表至少有两个结点
// 在多个节点的情况下,如果删除的是最后一个元素
if (toBeDeleted->next == nullptr) {
// 找待删除元素的前驱
ListNode* tmp = head;
while (tmp->next != toBeDeleted) {
tmp = tmp->next;
}
// 删除待结点
tmp->next = nullptr;
}
// 在多个节点的情况下,如果删除的是某个中间结点
else {
// 将下一个结点的值输入当前待删除的结点
toBeDeleted->value = toBeDeleted->next->value;
// 待删除的结点的下一个指向原先待删除引号的下下个结点,即将待删除的下一个结点删除
toBeDeleted->next = toBeDeleted->next->next;
}
// 返回删除节点后的链表头结点
return head;
}
2. 翻转单链表
题目描述:输出一个单链表的逆序反转后的链表。
解题思路:用三个临时指针 prev、cur、next 在链表上循环一遍即可。
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head == null)
return null;
ListNode pre = null;
ListNode next = null;
while(head != null){
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
3. 翻转部分单链表:
题目描述:给定一个单向链表的头结点head,以及两个整数from和to,在单链表上把第from个节点和第to个节点这一部分进行反转
举例:1->2->3->4->5->null, from = 2, to = 4 结果:1->4->3->2->5->null
ListNode* reverseBetween(ListNode* head, int m, int n) {
if (head == nullptr) return nullptr;
if (head->next == nullptr) return head;
int i = 1;
ListNode* reversedNewHead = nullptr;// 反转部分链表反转后的头结点
ListNode* reversedTail = nullptr;// 反转部分链表反转后的尾结点
ListNode* oldHead = head;// 原链表的头结点
ListNode* reversePreNode = nullptr;// 反转部分链表反转前其头结点的前一个结点
ListNode* reverseNextNode = nullptr;
while (head != nullptr) {
if (i > n) {
break;
}
if (i == m - 1) {
reversePreNode = head;
}
if (i >= m && i <= n) {
if (i == m) {
reversedTail = head;
}
reverseNextNode = head->next;
head->next = reversedNewHead;
reversedNewHead = head;
head = reverseNextNode;
} else {
head = head->next;
}
i++;
}
reversedTail->next = reverseNextNode;
if (reversePreNode != nullptr) {
reversePreNode->next = reversedNewHead;
return oldHead;
} else {
return reversedNewHead;
}
4. 旋转单链表K个位置
题目描述:给定一个单链表,设计一个算法实现链表向右旋转 K 个位置。 举例: 给定 1->2->3->4->5->6->NULL, K=3 则4->5->6->1->2->3->NULL 解题思路:
- 方法一 双指针,快指针先走k步,然后两个指针一起走,当快指针走到末尾时,慢指针的下一个位置是新的顺序的头结点,这样就可以旋转链表了。
- 方法二 先遍历整个链表获得链表长度n,然后此时把链表头和尾链接起来,在往后走n - k % n个节点就到达新链表的头结点前一个点,这时断开链表即可。
方法二代码:
public class Solution {
public ListNode* rotateRight(ListNode* head, int k) {
if (!head) return nullptr;
int n = 1;
ListNode* cur = head;
while (cur->next!=nullptr) {
++n;
cur = cur->next;
}
cur->next = head;
int m = n - k % n;
for (int i = 0; i < m; ++i) {
cur = cur->next;
}
ListNode* newhead = cur->next;
cur->next = nullptr;
return newhead;
}
};
5. 删除单链表倒数第 n 个节点
题目描述:删除单链表倒数第 n 个节点,1 <= n <= length,尽量在一次遍历中完成。 解题思路:双指针法,找到倒数第 n+1 个节点,将它的 next 指向倒数第 n-1个节点。
解题思路
经典的双指针法。定义两个指针,第一个指针从链表的头指针开始遍历向前走k-1
步,第二个指针保持不动,从第k
步开始,第二个指针也开始从链表的头指针开始遍历,由于两个指针的距离保持在k-1
,当第一个指针到达链表的尾节点时,第二个指针刚好指向倒数第k
个节点。
关注要点
-
链表头指针是否为空,若为空则直接返回回null
-
k是否为0,k为0也就是要查找倒数第0个节点,由于计数一般是从1开始的,所有输入0没有实际意义,返回null
-
k是否超出链表的长度,如果链表的节点个数少于k,则在指针后移的过程中会出现next指向空指针的错误,所以程序中要加一个判断
public class Solution { public ListNode* FindKthToTail(ListNode* head,int k) { if(head == nullptr || k == 0) return nullptr; ListNode* temp = head; //判断k是否超过链表节点的个数,注意是 i < k - 1 for(int i=0; i < k-1; i++){ if(temp->next != nullptr) temp = temp->next; else return nullptr; } ListNode* pA = temp; ListNode* pB = head; while (pA->next != nullptr) { pA = pA->next; pB = pB->next; } return pB; } }
6. 求单链表的中间节点
题目描述:求单链表的中间节点,如果链表的长度为偶数,返回中间两个节点的任意一个,若为奇数,则返回中间节点。 解题思路:快慢指针,慢的走一步,快的走两步,当快指针到达尾节点时,慢指针移动到中间节点。
// 遍历一次,找出单链表的中间节点
public ListNode* findMiddleNode(ListNode* head) {
if (nullptr == head) {
return;
}
ListNode* slow = head;
ListNode* fast = head;
while (nullptr != fast && nullptr != fast->next) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
7. 链表划分
题目描述: 给定一个单链表和数值x,划分链表使得所有小于x的节点排在大于等于x的节点之前。
public class Solution {
/**
* @param head: The first node of linked list.
* @param x: an integer
* @return: a ListNode
*/
public ListNode partition(ListNode* head, int x) {
// write your code here
if(head == null) return nullptr;
ListNode* leftDummy = new ListNode(0);
ListNode* rightDummy = new ListNode(0);
ListNode* left = leftDummy;
ListNode* right = rightDummy;
while (head != nullptr) {
if (head->val < x) {
left->next = head;
left = head;
} else {
right->next = head;
right = head;
}
head = head->next;
}
right->next = nullptr;
left->next = rightDummy->next;
return leftDummy->next;
}
8. 链表求和
题目描述:你有两个用链表代表的整数,其中每个节点包含一个数字。数字存储按照在原来整数中相反的顺序,使得第一个数字位于链表的开头。写出一个函数将两个整数相加,用链表形式返回和。 解题思路:做个大循环,对每一位进行操作:
当前位:(A[i]+B[i])%10 进位:(A[i]+B[i])/10
public class Solution {
public ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* c1 = l1;
ListNode* c2 = l2;
ListNode* sentinel = new ListNode(0);
ListNode* d = sentinel;
int sum = 0;
while (c1 != nullptr || c2 != nullptr) {
sum /= 10;
if (c1 != nullptr) {
sum += c1->val;
c1 = c1->next;
}
if (c2 != nullptr) {
sum += c2->val;
c2 = c2->next;
}
d->next = new ListNode(sum % 10);
d = d->next;
}
if (sum / 10 == 1)
d->next = new ListNode(1);
return sentinel->next;
}
9. 单链表排序
题目描述:在O(nlogn)时间内对链表进行排序。
快速排序:
ListNode* sortList(ListNode* head) {
//采用快速排序
quickSort(head, nullptr);
return head;
}
void quickSort(ListNode* head, ListNode* end) {
if (head != end) {
ListNode* node = partion(head, end);
quickSort(head, node);
quickSort(node->next, end);
}
}
ListNode* partion(ListNode* head, ListNode* end) {
ListNode* p1 = head;
ListNode* p2 = head->next;
//走到末尾才停
while (p2 != end) {
//大于key值时,p1向前走一步,交换p1与p2的值
if (p2->val < head->val) {
p1 = p1->next;
std::swap(p1->val, p2->val);
}
p2 = p2->next;
}
//当有序时,不交换p1和key值
if (p1 != head) {
std::swap(p1->val, head->val);
}
return p1;
}
归并排序:
public ListNode sortList(ListNode head) {
//采用归并排序
if (head == null || head.next == null) {
return head;
}
//获取中间结点
ListNode mid = getMid(head);
ListNode right = mid.next;
mid.next = null;
//合并
return mergeSort(sortList(head), sortList(right));
}
/**
* 获取链表的中间结点,偶数时取中间第一个
*
* @param head
* @return
*/
private ListNode getMid(ListNode head) {
if (head == null || head.next == null) {
return head;
}
//快慢指针
ListNode slow = head, quick = head;
//快2步,慢一步
while (quick.next != null && quick.next.next != null) {
slow = slow.next;
quick = quick.next.next;
}
return slow;
}
/**
*
* 归并两个有序的链表
*
* @param head1
* @param head2
* @return
*/
private ListNode mergeSort(ListNode head1, ListNode head2) {
ListNode p1 = head1, p2 = head2, head;
//得到头节点的指向
if (head1.val < head2.val) {
head = head1;
p1 = p1.next;
} else {
head = head2;
p2 = p2.next;
}
ListNode p = head;
//比较链表中的值
while (p1 != null && p2 != null) {
if (p1.val <= p2.val) {
p.next = p1;
p1 = p1.next;
p = p.next;
} else {
p.next = p2;
p2 = p2.next;
p = p.next;
}
}
//第二条链表空了
if (p1 != null) {
p.next = p1;
}
//第一条链表空了
if (p2 != null) {
p.next = p2;
}
return head;
}
复制代码
10. 合并两个排序的链表
题目描述:输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
if(pHead1 == nullptr)
return pHead2;
else if(pHead2 == nullptr)
return pHead1;
ListNode* pMergedHead = nullptr;
if(pHead1->value < pHead2->value)
{
pMergedHead = pHead1;
pMergedHead->next = Merge(pHead1->next, pHead2);
}
else
{
pMergedHead = pHead2;
pMergedHead->next = Merge(pHead1, pHead2->next);
}
return pMergedHead;
}
11. 复杂链表的复制
题目描述:输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
12. 删除链表中重复的结点
题目描述:在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
虽然这个方法可以,但是时间复杂度为O(N), 空间复杂度为一个哈希表的结构,所以采用这种不是最佳选择。
如果cur值与nex值不相等,那么就可以三个指针各自往前移动一个。
ListNode* deleteDuplication(ListNode* pHead) { // 先判断空 if (pHead == NULL) { return NULL; } // 判断是否只有一个节点 if (pHead->next == NULL) { return pHead; } // 我们采用带头链表,自己添加一个头 ListNode* pre = new ListNode(); pre->next = pHead; // 把头节点链接在链表上 ListNode* pre_head = pre; // 用来保存头节点,用于返回删除后的链表 ListNode* cur = pHead; //中指针 ListNode* nex = pHead->next; // 后面指针 while (nex != nullptr) // 结束条件 while (nex != nullptr && cur->val == nex->val) { nex = nex->next; } // 如果没有重复的那么cur的next一定等于nex if (cur->next != nex) // 如果相等说明没有相同的节点 { while (cur != nex) // 删除动作 { pre->next = cur->next; delete cur; cur = pre->next; } if (nex != nullptr) // 这里一定要要注意,要防止走到NULL发生段错误 nex = nex->next; } else { // 处理没有重复的情况 pre = cur; nex = nex->next; cur = cur->next; } } ListNode* head = pre_head->next; // 释放空间,防止内存泄漏 delete pre_head; return head; }
我们测试要尽可能的全面1)传NULL2)只有一个节点3)头节点开始就有重复4)中间节点重复5)尾部节点重复6)链表中没有重复链表7)所有节点都是重复的
13. 判断单链表是否存在环
题目描述:判断一个单链表是否有环 分析:快慢指针,慢指针每次移动一步,快指针每次移动两步,如果存在环,那么两个指针一定会在环内相遇。
14. 单链表是否有环扩展:找到环的入口点
题目描述:判断单链表是否有环,如果有,找到环的入口点
解法1:
新建一个HashSet
,遍历链表,每次尝试添加当前遍历的节点到HashSet
,如果添加失败,则代表HashSet
中已经包含该节点,此时的节点即为环的入口点.
代码如下:
ListNode* solution1(ListNode *pHead) {
ListNode* node = pHead;
unordered_set<ListNode*> set;
while (node != null) {
if (!set.add(node)) {
return node;
}
node = node->next;
}
return nullptr;
}
因为额外申请了内存,所以空间复杂度为O(n),时间复杂度为O(n)
解法2:
- 首先判断链表中是否有环,可以定义两个指针,一个每次走一步,一个每次走两步,如果快指针走到链表尾部时,两个指针没有相遇,则代表无环,否则说明又环.
- 确定环中节点的个数n.
- 将两个指针移动到链头,一个先走n步,然后两个指针一起走,如果两个指针相遇,则相遇点就是环的入口点.
代码如下:
ListNode* solution2(ListNode* pHead) {
ListNode* fast = pHead;
ListNode* slow = pHead;
while (fast != nullptr && fast->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) {
fast = pHead;
while (slow != fast) {//最终在环里循环,直到相遇
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
}
return nullptr;
}
此处并没有严格按照1,2,3的步骤,因为第一次两个指针相遇的节点处,就是3中先走的n步,假设slow指针走了x
步,那么第一次相遇的时候,fast走了2x
步,如果环中有n个节点,则fast比slow多走了n步,即2x-n=x
,则x=n
,所以两个指针第一次相遇的节点就是一个指针从链表头走了环中节点个数n
步的位置.
这里并没有额外申请空间,只是用了两个指针变量,所以空间复杂度为O(1),时间复杂度为O(n)
15. 判断两个无环单链表是否相交
题目描述:给出两个无环单链表 解题思路:
- 方法一 最直接的方法是判断 A 链表的每个节点是否在 B 链表中,但是这种方法的时间复杂度为 O(Length(A) * Length(B))。其实借助unordered_set只要O(Length(A) + Length(B))
- 方法二 转化为环的问题。把 B 链表接在 A 链表后面,如果得到的链表有环,则说明两个链表相交。可以之前讨论过的快慢指针来判断是否有环,但是这里还有更简单的方法。如果 B 链表和 A 链表相交,把 B 链表接在 A 链表后面时,B 链表的所有节点都在环内,所以此时只需要遍历 B 链表,看是否会回到起点就可以判断是否相交。这个方法需要先遍历一次 A 链表,找到尾节点,然后还要遍历一次 B 链表,判断是否形成环,时间复杂度为 O(Length(A) + Length(B))。
- 方法三 除了转化为环的问题,还可以利用“如果两个链表相交于某一节点,那么之后的节点都是共有的”这个特点,如果两个链表相交,那么最后一个节点一定是共有的。所以可以得出另外一种解法,先遍历 A 链表,记住尾节点,然后遍历 B 链表,比较两个链表的尾节点,如果相同则相交,不同则不相交。时间复杂度为 O(Length(A) + Length(B)),空间复杂度为 O(1),思路比解法 2 更简单。
方法三的代码:
boolean isIntersect(ListNode *headA, ListNode *headB) {
if (nullptr == headA || nullptr == headB) {
return false;
}
if (headA == headB) {
return true;
}
while (nullptr != headA->next) {
headA = headA->next;
}
while (nullptr != headB->next) {
headB = headB->next;
}
return headA == headB;
}
16. 两个链表相交扩展:求两个无环单链表的第一个相交点
题目描述:找到两个无环单链表第一个相交点,如果不相交返回空,要求在线性时间复杂度和常量空间复杂度内完成。 解题思路:
- 方法一 如果两个链表存在公共结点,那么它们从公共结点开始一直到链表的结尾都是一样的,因此我们只需要从链表的结尾开始,往前搜索,找到最后一个相同的结点即可。但是题目给出的单向链表,我们只能从前向后搜索,这时,我们就可以借助栈来完成。先把两个链表依次装到两个栈中,然后比较两个栈的栈顶结点是否相同,如果相同则出栈,如果不同,那最后相同的结点就是我们要的返回值。
- 方法二 先找出2个链表的长度,然后让长的先走两个链表的长度差,然后再一起走,直到找到第一个公共结点。
- 方法三 由于2个链表都没有环,我们可以把第二个链表接在第一个链表后面,这样就把问题转化为求环的入口节点问题。
- 方法四 两个指针p1和p2分别指向链表A和链表B,它们同时向前走,当走到尾节点时,转向另一个链表,比如p1走到链表 A 的尾节点时,下一步就走到链表B,p2走到链表 B 的尾节点时,下一步就走到链表 A,当p1==p2 时,就是链表的相交点
方法四的代码:
ListNode* getIntersectionNode(ListNode *headA, ListNode *headB) {
if (nullptr == headA || nullptr == headB) {
return null;
}
if (headA == headB) {
return headA;
}
ListNode p1 = headA;
ListNode p2 = headB;
while (p1 != p2) {
// 遍历完所在链表后从另外一个链表再开始
// 当 p1 和 p2 都换到另一个链表时,它们对齐了:
// (1)如果链表相交,p1 == p2 时为第一个相交点
// (2)如果链表不相交,p1 和 p2 同时移动到末尾,p1 = p2 = nullptr,然后退出循环
p1 = (nullptr == p1) ? headB : p1->next;
p2 = (nullptr == p2) ? headA : p2->next;
}
return p1;
}
17. 两个链表相交扩展:判断两个有环单链表是否相交
题目描述:上面的问题是针对无环链表的,如果是链表有环呢?
解题思路:如果两个有环单链表相交,那么它们一定共有一个环,即环上的任意一个节点都存在于两个链表上。因此可以先用之前快慢指针的方式找到两个链表中位于环内的两个节点,如果相交的话,两个节点在一个环内,那么移动其中一个节点,在一次循环内肯定可以与另外一个节点相遇。
如果两个链表无环,判断是否相交很简单,判断两个环的最后一个节点指针是否相等即可。
分析:如果有环且两个链表相交,则两个链表都有共同一个环,即环上的任意一个节点都存在于两个链表上。因此,就可以判断一链表上俩指针相遇的那个节点,在不在另一条链表上。
无环链表和有环链表是不可能相交的;
两个有环链表若相交,其“整个环上”的所有node一定都重合;
有环链表的相交,情况只有2种:相交于”不是环的部分”或相交于”环上“,即下图所示;
//判断单链表是否存在环,参数circleNode是环内节点,后面的题目会用到
bool hasCircle(Node *head,Node *&circleNode)
{
Node *slow,*fast;
slow = fast = head;
while(fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
circleNode = fast;
return true;
}
}
return false;
}
//判断两个带环链表是否相交
bool isIntersectWithLoop(Node *h1,Node *h2)
{
Node *circleNode1,*circleNode2;
if(!hasCircle(h1,circleNode1)) //判断链表带不带环,并保存环内节点
return false; //不带环,异常退出
if(!hasCircle(h2,circleNode2))
return false;
Node *temp = circleNode2->next;
while(temp != circleNode2)
{
if(temp == circleNode1)
return true;
temp = temp->next;
}
return false;
}
18.孩子们的游戏(圆圈中最后剩下的数)
描述
每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)
如果没有小朋友,请返回-1
示例1
输入:5,3
复制返回值:3
方法一:模拟
最开始长度为n,每次删除一个数,长度变为n-1,如果用数组模拟操作的话,删除一个数据,涉及大量的数据搬移操作,所以我们可以使用链表来模拟操作。
代码如下:
class Solution {
public:
int LastRemaining_Solution(int n, int m)
{
if (n <= 0) return -1;
list<int> lt;
for (int i=0; i<n; ++i)
lt.push_back(i);
int index = 0;
while (n > 1) {
index = (index + m - 1) % n;
auto it = lt.begin();
std::advance(it, index); // 让it向后移动index个位置
lt.erase(it);
--n;
}
return lt.back();
}
};
时间复杂度:O(N^2), 每次删除一个节点,需要先找到那个节点,然后再删除,查找的时间复杂度为O(N)
空间复杂度:O(N)
方法二:递归
假设f(n, m) 表示最终留下元素的序号。比如上例子中表示为:f(5,3) = 3
首先,长度为 n 的序列会先删除第 m % n 个元素,然后剩下一个长度为 n - 1 的序列。那么,我们可以递归地求解 f(n - 1, m),就可以知道对于剩下的 n - 1 个元素,最终会留下第几个元素,我们设答案为 x = f(n - 1, m)。
由于我们删除了第 m % n 个元素,将序列的长度变为 n - 1。当我们知道了 f(n - 1, m) 对应的答案 x 之后,我们也就可以知道,长度为 n 的序列最后一个删除的元素,应当是从 m % n 开始数的第 x 个元素。因此有 f(n, m) = (m % n + x) % n = (m + x) % n。
当n等于1时,f(1,m) = 0
代码为:
class Solution {
public:
int LastRemaining_Solution(int n, int m)
{
if (n <= 0) return -1;
if (n == 1) return 0;
int x = LastRemaining_Solution(n-1, m);
return (x+m) % n;
}
};
时间复杂度:O(N)
空间复杂度: O(N)
方法三:迭代法
根据方法二可知,
f[1] = 0
f[2] = (f{1] + m) % 2
f[3] = (f[2] + m) % 3
...
f[n] = (f[n-1] + m) % n
所以代码如下:
class Solution {
public:
int LastRemaining_Solution(int n, int m)
{
if (n <= 0) return -1;
int index = 0;
for (int i=2; i<=n; ++i) {
index = (index + m) % i;
}
return index;
}
};
时间复杂度:O(N)
空间复杂度: O(1)
19.扑克牌顺子
描述
现在有2副扑克牌,从扑克牌中随机五张扑克牌,我们需要来判断一下是不是顺子。
有如下规则:
1. A为1,J为11,Q为12,K为13,A不能视为14
2. 大、小王为 0,0可以看作任意牌
3. 如果给出的五张牌能组成顺子(即这五张牌是连续的)就输出true,否则就输出false。
例如:给出数据[6,0,2,0,4]
中间的两个0一个看作3,一个看作5 。即:[6,3,2,5,4]
这样这五张牌在[2,6]区间连续,输出true
数据保证每组5个数字,每组最多含有4个零,数组的数取值为 [0, 13]
方法一:set+遍历
我们分两种情况考虑,
一. 如果vector中不包含0的情况:
那么如何判断呢?因为需要是顺子,所以首先不能有重复值, 如果没有重复值,那么形如[1 2 3 4 5]
[5 6 7 8 9]
, 会发现最大值与最小值的差值应该小于5.
二. 如果vector中包含0:
发现除去0后的值,判断方法和1中是一样的。
所以根据如上两个条件,算法过程如下:
- 初始化一个
set
,最大值max_ = 0, 最小值min_ = 14 - 遍历数组, 对于大于0的整数,没有在set中出现,则加入到set中,同时更新max_, min_
- 如果出现在了
set
中,直接返回false
- 数组遍历完,最后再判断一下最大值与最小值的差值是否小于5
代码如下:
class Solution {
public:
bool IsContinuous( vector<int> numbers ) {
if (numbers.empty()) return false;
set<int> st;
int max_ = 0, min_ = 14;
for (int val : numbers) {
if (val > 0) {
if (st.count(val) > 0) return false;
st.insert(val);
max_ = max(max_, val);
min_ = min(min_, val);
}
}
return max_ - min_ < 5;
}
};
时间复杂度:O(N)
空间复杂度:O(N)
方法二:排序+遍历
根据方法一的分析,实现上如果不用set判断是否有重复值的话,还可以先排序,然后如果有重复值,那么肯定相邻。
所以代码如下:
class Solution {
public:
bool IsContinuous( vector<int> numbers ) {
if (numbers.empty()) return false;
sort(numbers.begin(), numbers.end());
int i = 0, sz = numbers.size();
for (int j=0; j<sz; ++j) {
if (numbers[j] == 0) {
++i; // i 记录最小值的下标
continue;
}
if (j+1<sz && numbers[j] == numbers[j+1]) return false;
}
return numbers.back() - numbers[i] < 5;
}
};
时间复杂度:O(NlogN)
空间复杂度:O(1)
20. 和为S的两个数字
描述
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
返回值描述:
对应每个测试案例,输出两个数,小的先输出。
方法一:哈希法
要求a + b = sum, 如果已知a, 那么b = sum - a
所以可以先将b添加入哈希中,然后遍历一遍数组设为a, 在哈希中寻找是否存在sum-a,然后再更新乘积最小值
代码
class Solution {
public:
vector<int> FindNumbersWithSum(vector<int> array,int sum) {
if (array.empty()) return vector<int>();
int tmp = INT_MAX;
pair<int, int> ret;
unordered_map<int,int> mp;
for (int i=0; i<array.size(); ++i) {
mp[array[i]] = i;
}
for (int i=0; i<array.size(); ++i) {
if (mp.find(sum-array[i]) != mp.end()) {
int j = mp[sum-array[i]];
if ( j > i && array[i]*array[j] < tmp) {
tmp = array[i] * array[j];
ret = {i, j};
}
}
}
if (ret.first == ret.second) return vector<int>();
return vector<int>({array[ret.first], array[ret.second]});
}
};
时间复杂度:O(n)
空间复杂度:O(n)
方法二:双指针
假设:若b>a,且存在,
a + b = s;
(a - m ) + (b + m) = s
则:(a - m )(b + m)=ab - (b-a)m - m*m < ab;说明外层的乘积更小
也就是说依然是左右夹逼法!!!只需要2个指针
1.left开头,right指向结尾
2.如果和小于sum,说明太小了,left右移寻找更大的数
3.如果和大于sum,说明太大了,right左移寻找更小的数
4.和相等,把left和right的数返回
//开始还在纠结乘积最小,后来转念一想,a+b=sum,a和b越远乘积越小,而一头一尾两个指针往内靠近的方法找到的就是乘积最小的情况。
//如果是乘积最大的情况就是一直找到两个指针重合,每次找到一个就将之前返回的结果向量清空然后更新为新找到的。
class Solution {
public:
//开始还在纠结乘积最小,后来转念一想,a+b=sum,a和b越远乘积越小,而一头一尾两个指针往内靠近的方法找到的就是乘积最小的情况。
//如果是乘积最大的情况就是一直找到两个指针重合,每次找到一个就将之前返回的结果向量清空然后更新为新找到的。
vector<int> FindNumbersWithSum(vector<int> array,int sum) {
vector<int> result;
int length = array.size();
int start = 0;
int end = length - 1;
while (start < end) {
if (array[start] + array[end] == sum) {
result.push_back(array[start]);
result.push_back(array[end]);
break;
} else if (array[start] + array[end] < sum) {
start++;
} else {
end--;
}
}
return result;
}
};
代码
时间复杂度:O(n)
空间复杂度:O(1)
21. 和为S的连续正数序列
描述
小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!
返回值描述:
输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序
输入:9
返回值:[[2,3,4],[4,5]
方法三:滑动窗口
知识补充:
-
什么是滑动窗口?
顾名思义,首先是一个窗口,既然是一个窗口,就需要用窗口的左边界i
和右边界j
来唯一表示一个窗口,其次,滑动代表,窗口始终从左往右移动,这也表明左边界i
和右边界j
始终会往后移动,而不会往左移动。
这里我用左闭右开区间来表示一个窗口。比如
-
滑动窗口的操作
- 扩大窗口,
j += 1
- 缩小窗口,
i += 1
算法步骤:
- 初始化,
i=1,j=1
, 表示窗口大小为0
- 如果窗口中值的和小于目标值sum, 表示需要扩大窗口,
j += 1
- 否则,如果狂口值和大于目标值sum,表示需要缩小窗口,
i += 1
- 否则,等于目标值,存结果,缩小窗口,继续进行步骤
2,3,4
这里需要注意2个问题:
- 什么时候窗口终止呢,这里窗口左边界走到sum的一半即可终止,因为题目要求至少包含2个数
- 什么时候需要扩大窗口和缩小窗口?解释可看上述算法步骤。
代码如下:
class Solution {
public:
vector<vector<int> > FindContinuousSequence(int sum) {
vector<vector<int> > allRes;
int phigh = 2, plow = 1;
while (phigh > plow) {
//由于是连续的,差为1的一个序列,那么求和公式是(a0+an)*n/2
int cur = (phigh+plow)*(phigh-plow+1)/2;
//如果当前窗口内的值之和小于sum,那么右边窗口右移一下
if (cur < sum) {
phigh++;
} else if (cur > sum) {
//如果当前窗口内的值之和大于sum,那么左边窗口右移一下
plow++;
} else {
//相等,那么就将窗口范围的所有数添加进结果集,左边窗口右移一下
vector<int> res;
for(int i = plow; i<=phigh; i++)
res.push_back(i);
allRes.push_back(res);
plow++;
}
}
}
};
时间复杂度:O(N)
空间复杂度:O(1)