线性表相关
【2009年真题】
【解析】
快慢指针思想:
- 定义两个指针,快指针 f a s t fast fast, 慢指针 l o w low low
- 让 f a s t fast fast 先向前移动 k k k 个位置,然后 l o w low low 和 f a s t fast fast 再一起向前移动 .
- 当 f a s t fast fast 到达链表尾部,返回 l o w low low
struct ListNode{
int val;
ListNode *next;
ListNode(int x):val(x),next(nullptr){}
};
ListNode* helper(ListNode *head, int k){
ListNode *fast = head;
ListNode *slow = head;
while(fast!=nullptr){
fast = fast->next;
if(!k) low = low->next;
else k--;
}
return slow;
}
int Solution(ListNode *head, int k){
ListNode *tmp = helper(head, k);
if(tmp)
{
cout << tmp->val;
return 1;
}
else return 0;
}
【2010年真题】
【解析】
局部翻转的思想:
假设前 p p p 个元素构成子序列 a a a,那么题目说描述问题既是把数组 a b ab ab 翻转得到数组 b a ba ba,于是 b a = ( a − 1 b − 1 ) − 1 ba=(a^{-1}b^{-1})^{-1} ba=(a−1b−1)−1
void helper(vector<int> &nums, int from, int to){
int tmp;
for(int i=0;i<(to-from+1)/2;i++)
{
tmp = nums[from+i];
nums[from+i] = nums[to-i];
nums[to-i] = tmp;
}
}
void Solution(vector<int> &nums,int p){
int n = nums.size();
helper(nums, 0, p-1);
helper(nums, p-1, n);
helper(nums, 0, n-1);
}
复杂度分析: 三个 helper
函数的时间复杂度为
O
(
p
/
2
)
,
O
(
(
n
−
p
)
/
2
)
,
O
(
n
/
2
)
O(p/2),O((n-p)/2),O(n/2)
O(p/2),O((n−p)/2),O(n/2),故所设计算法的时间复杂度为
O
(
n
)
O(n)
O(n);空间复杂度为
O
(
1
)
O(1)
O(1)
【2011真题】
【解析】
分别求出序列 A 和 B 的中位数,设为 a 和 b,求序列 A 和 B 的中位数过程如下:
- 若 a = b,则 a 或 b 即为所求中位数,算法结束
- 若 a < b,则舍弃序列 A 中较小的一半,同时舍弃序列 B 中较大的一半,要求舍弃的长度相等
- 若 a>b,则舍弃序列 A 中较大的一半,同时舍弃序列 B 中较小的一半,要求舍弃的长度相等
- 在保留的两个升序序列中,重复过程 上面三个步骤,直到两个序列中只含一个元素时为止,较小者即为所求的中位数
int Solutoin(vector<int> &A,vector<int> &B){
int n = A.size();
int s1 = 0, d1=n-1, s2 = 1, d2 = n-1;
int m1, m2;
while(s1!=d1||s2!=d2)
{
m1 = (s1+d1)/2;
m2 = (s2+d2)/2;
if(A[m1]==B[m2]) return A[m1];
if(A[m1]<B[m2])
{
if((s1+d1)%2==0)
{
s1 = m1;
d2 = m2;
}
else
{
s1 = m1+1;
d2 = m2;
}
}
else
{
if((s1+d1)%2==0)
{
d1 = m1;
s2 = m2;
}
else
{
d1 = m1+1;
s2 = m2;
}
}
}
return A[s1]<B[s2]?A[s1]:B[s2];
}
复杂度分析: 时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n),空间复杂度为 O ( 1 ) O(1) O(1)
我能想到的算法:
先求得两个序列的总长度,然后对应一半计算出中位数的下标(注意是向上取整),然后对两个升序列取两指针游走比较舍去中位数之前的元素。这样下来,时间复杂度是 O ( n + m ) O(n+m) O(n+m),空间复杂度为 O ( 1 ) O(1) O(1)
【2012真题】
【解析】
算法的思路很简单,就是:
- 初始化 ha = headA, hb = headB,开始遍历。
- 若A链短,ha 会先到达链表尾,当 ha 到达末尾时,重置 ha 为 headB;同样的,当 hb 到达末尾时,重置 hb 为 headA;当 ha 与 hb 相遇时,必然就是两个链表的交点
为什么这么做可以做呢?
因为这样的一个遍历过程,对 ha 而言,走过的路程即为 a+c+b,对 hb 而言,即为 b+c+a,显然 a+c+b = b+c+a,这就是该算法的核心原理。
ListNode* getIntersectionNode(ListNode* srt1, ListNode* str2){
ListNode *first1 = str1->next, *first2 = str2->next;
ListNode *p = first1, *q = fisrt2;
while(p->data!=q->data){
p = p==nullptr?first2:p->next;
q = q==nullptr?first1:q->next;
}
return p;
}
复杂度分析: 时间复杂度为 O ( m + n ) O(m+n) O(m+n)
【2013真题】
【解析】
第一步是一个非常重要的子模块,如何查找一个数组中出现次数最多的元素,就采用此办法
int Solution(vector<int> nums>{
int c = nums[0], n = nums.size(), cnt = 1;
for(int i=1;i<n;i++)
{
if(nums[i]==c) cnt++;
else
{
if(cnt) cnt--;
else
{
c = A[i];
cnt = 1;
}
}
}
if(cnt)
{
cnt = count(nums.begin(), nums.end(), c);
if(cnt>n/2) return c;
else return -1;
}
}
复杂度分析: 时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)
我能想到的算法:
计数排序,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)
【2015 年真题】
【解析】
打表,以空间换时间
ListNode *Solution(ListNode *head, int n){
vectot<int> flag(n, 0);
ListNode *p = head, *t;
int tmp;
while(p->next!=nullptr)
{
tmp = p->next->data > 0 ? p->next->data:-p->next->data;
if(flag[tmp])
{
t = p->next;
p->next = t->next;
delete t;
t = nullptr;
}
else
{
p = p->next;
flag[tmp] = 1;
}
}
return head;
}
复杂度分析: 时间复杂度为 O ( m ) O(m) O(m),空间复杂度为 O ( n ) O(n) O(n)
【2018真题】
【解析】
取一个标志数组,记录数组中已经出现的正整数,由此设计的算法时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)
int Solution(vector<int> nums){
int n = nums.size();
vector<int> flag(n, 0);
for(auto num: nums)
if(num>0 && num<=n) flag[num-1] = 1;
int i;
for(i=0;i<n;i++)
if(!flag[i]) break;
return i+1;
}
【2019年真题】
【解析】
思路是这样的:把链表的后半段逆序,于是就可以从前半段的正序部分和后半段的逆序部分构造出 L ′ L' L′
在实际操作中,
- 我们需要先利用快慢指针找到链表的中间结点(慢指针走一步,快指针走两步)
- 然后对后半段链表原地逆序
- 按要求交叉合并前半段和后半段
NODE* reverseList(NODE* head){
NODE* pre = nullptr;
NODE* cur = head;
NODE* tmp = nullptr;
while(cur)
{
tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
NODE* middleNode(NODE* head){
NODE *p = head, *q = head;
while(q!=nullptr&&q->next!=nullptr)
{
p = p->next;
q = q->next->next;
}
return p;
}
NODE* Solution(NODE* head){
NODE* second_half = middle(head);
second_half = reverList(second->half);
NODE *ptr1 = head->next, *ptr2 = second_half;
while(ptr2)
{
second_half = ptr2->next;
ptr2->next = ptr1->next;
ptr1->next = ptr2;
ptr1 = ptr2->next;
ptr2 = second_half;
}
return head;
}
本题中出现的两个子函数模块是十分重要的,请加强重视
复杂度分析: 时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1) 满足题目要求
【2020年真题】
【解析】
【习题】
【解析】快慢指针思想,慢指针走一步,快指针走两步
ListNode* middleNode(ListNode* head) {
ListNode *p = head, *q = head;
while(q!=nullptr && q->next!=nullptr)
{
p = p->next;
q = q->next->next;
}
return p;
}
【习题】
【解析】
ListNode* reverseList(ListNode* head){
ListNode* pre = nullptr;
ListNode* cur = head;
ListNode* tmp = nullptr;
while(cur)
{
tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
【习题】
【解析】
void solution(ListNode* &head){
ListNode* pre = nullptr;
ListNode* cur = head ->next;
ListNode* tmp = nullptr;
while(cur!=nullptr)
{
tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
head->next = pre;
}
【习题】
【解析】
数组和单链表的处理是一样的,这里给出一种迭代的解法,时间复杂度是 O ( m + n ) O(m+n) O(m+n),空间复杂度是 O ( 1 ) O(1) O(1)(如果是数组的话,空间复杂度为 O ( m + n ) O(m+n) O(m+n))
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2){
ListNode* dummyhead = new ListNode(-1);
ListNode* p = dummyhead;
while(l1!=nullptr&&l2!=nullptr)
{
if(l1->val<l2->val)
{
p->next = l1;
l1 = l1->next;
}
else
{
p->next = l2;
l2 = l2->next;
}
p = p->next;
}
p->next = l1?l1:l2;
return dummyhead->next;
}
【习题】
【解法】
ListNode* difference(ListNode *A, ListNode *B){
ListNode *p = A->next;
ListNode *q = B->next;
ListNode *pre = A;
while(p!=nullptr && q!=nullptr)
{
if(p->val<q->val)
{
pre =p;
p = p->next;
}
else if(p->val>q->val) q = q->next;
else
{
pre->next = p->next;
ListNode *tmp = p;
p = p->next;
delete tmp;
tmp = nullptr;
}
}
return A;
}
【习题】
【解析】
void swap(Sqlist& L, int i, int j){
int tmp;
tmp = L.data[i];
L.data[i] = L.data[j];
L.data[j] = tmp;
}
void reverse(Sqlist& L){
for(int i=0,j=L.length-1;i<j;i++,j--)
swap(L, i, j);
}
【习题】
【解析】
void deleteij(Sqlist& L, int i, int j){
for(int k=j+1;k<L.length-1;k++)
L.data[i++] = L.data[k];
L.length -= (j-i+1);
}
【习题】
【解析】
void solution(Sqlist &L){
int i = 0, j = L.length-1;
int tmp = L.data[i];
while(i<j)
{
while(i<j && L.data[j]>tmp) j--;
if(i<j)
{
L.data[i] = L.data[j];
i++;
}
while(i<j && L.data[i]<tmp) i++;
if(i<j)
{
L.data[j] = L.data[i];
j--;
}
}
L.data[i] = tmp;
}
【习题】
【解析】
void solution(ListNode* &head){
ListNode* p = head->next;
ListNode* q = nullptr;
while(p>next!=nullptr)
{
if(p->val==p->next->val)
{
q = p->next;
p->next = q->next;
delete q;
q = nullptr;
}
else p = p->next ;
}
}
【注】如果链表无序的话,用打表的方式更好
【习题】
【解析】
void solution(ListNode* &head){
ListNode* pre = head;
ListNode* p = heda->next;
ListNode* tmp = p;
ListNode* tmp_pre = pre;
while(p!=nullptr)
{
if(p->val<tmp->val)
{
tmp = p;
tmp_pre = pre;
}
pre = p;
p = p->next;
}
tmp_pre->next = tmp->next;
delete tmp;
tmp = nullptr;
}
【习题】
【解析】
ListNode* solution(ListNode* &A){
ListNode *p, *q, *r
ListNode* B= new ListNode(-1) ;
r = B;
p = A;
while(p->next!=nullptr)
{
if(p->next->data%2==0)
{
q = p->next;
p->next = p->next->next;
q->next = nullptr;
r->next = q;
r= q;
}
else p = p->next;
}
return B;
}
【习题】
【解析】
数组找最小值不是难题,一个循环加一个辅助变量就可以找到最小的元素了。但是这道题有一个限制的地方就在于没有两个变量可用。于是, i i i 就担负起了一个变量两个任务的责任,我们取 i i i 为一个两位的数字,其十位数字表示循环控制变量,个位数字表示当前位置的元素值
int solution(vector<int> A, int N){
int i = A[0];
while(i/10<=N-1)
{
if(i%10>A[i/10])
{
i -= i%10;
i += A[i/10];
}
i += 10;
}
return i%10;
}
【习题】
【解析】
和反转链表一样的,先反转后打印,当然如果直接用递归反转链表可以不用拆分为两步
void solution(ListNode* head){
if(head!=nullptr)
{
solution(head->next);
cout << head->val << " ";
}
}
【习题】
【解析】
一趟遍历,最坏情况下(从大到小)比较次数为 2 ( n − 1 ) 2(n-1) 2(n−1) 次,最好情况下(从小到大)比较次数为 n − 1 n-1 n−1 次,平均一下 2 ( n − 1 ) + ( n − 1 ) 2 = 3 n 2 − 3 2 < 3 n 2 \frac{2(n-1)+(n-1)}{2}=\frac{3n}{2}-\frac{3}{2}<\frac{3n}{2} 22(n−1)+(n−1)=23n−23<23n,所以就一趟遍历,没那么多事
【习题】
【解析】
int solution(vector<char>& A, vector<char>& B){
int i = 0, n = A.size(), m = B.size();
while(i<n && i<m)
{
if(A[i]==B[i]) i++;
else break;
}
if(i>=n && i>=m) return 0;
else if((i>=n && i<m) || (A[i]<B[i])) return -1;
return 1;
}
【习题】
【解析】
一句话解决的事,剑指offer - 链表中倒数第k个节点,快慢指针,直接看代码
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode* fast = head;
ListNode* low = head;
while(fast!=NULL)
{
fast = fast->next;
if(k==0) low = low->next;
else k--;
}
return low;
}
};
【习题】
【解析】
void reverse(vector<int>& R, int l, int r){
int tmp;
for(int i=1,j=r;i<j;i++,j--)
{
tmp = R[i];
R[i] = R[j];
R[j] = tmp;
}
}
void solution(vector<int>& R, int p){
int n = R.size();
if(p<=0 || p>=n) cout << "ERROR" << endl;
else
{
reverse(R, 0, p-1);
reverse(R, p, n-1);
reverse(R, 0, n-1);
}
}
【习题】
【解析】
int solution(vector<int>& A){
int c = A[0], n = A.size(), cnt = 1;
for(int i=1;i<n;i++)
{
if(A[i]==c) cnt ++;
else
{
if(cnt) cnt--;
else
{
c = A[i];
cnt = 1;
}
}
}
if(cnt)
{
cnt = 0;
for(int i=0;i<n;i++)
if(A[i]==c) cnt++;
}
if(cnt>n/2) return c;
else return -1;
}
【关于候选主元素的选取再补充一点】
我们假定某个元素是主元素,当下一个元素和它相等时,我们就让计数变量加一,不等就减一,当计数变量变为 0 的时候,我们就需要重新选取我们的假定主元素重复上面的过程
栈和队列相关
【习题】有效括号匹配
【解析】
bool Solution(string s){
unordered_map<char, char> pairs {{'(', ')'}, {'[', ']'}, {'{', '}'}, {'?', '?'}};
unordered_set<char> brackets {'(', ')', '[', ']', '{', '}'};
vector<char> st;
st.push_back('?');
for(char i: s)
{
if(brackets.count(i))
{
if(pairs.count(i)) st.push_back(i);
else if(pairs[st.back()]==i) st.pop_back();
else return false;
}
}
return st.size()==1;
}
【习题】计算括号的深度
【解析】
维护一个栈 s,从左至右遍历括号字符串中的每一个字符:
- 如果当前字符是 (,就把 ( 压入栈中,此时这个 ( 的嵌套深度为栈的高度;
- 如果当前字符是 ),此时这个 ) 的嵌套深度为栈的高度,随后再从栈中弹出一个 (
例如.
【习题】
【解析】
1)两条规则:I 的个数和 O 的个数相等:从给定序列的开始到序列中的任一位置,I 的个数要大于或等于 O 的个数
2)能,如
输入 ABC,IOIOIO,输出为 ABC
输入 BAC,IIOOIO,输出为 ABC
3)
int judge(char ch[]) {
int i = 0, o = 0;
for(auto c: ch)
{
if(c=='\0') break;
else
{
if(c=='I') i++;
else o++;
if(o>i) return 0;
}
}
if(i!=o) return 0;
return 1;
}
【习题】
【解析】
知道尾指针后,实现元素入队,则直接在链表尾插入就行。而实现出队,而需要根据尾指针找到头结点和开始节点,然后进行删除。要注意的是,尾指针应始终指向终端结点,并且当删除结点后队列为空时,必须特殊处理。
void Enque(ListNode* &rear, int x){
ListNode* s = new ListNode(x);
s->next = rear->next;
rear->next = s;
rear = s;
}
bool Deque(ListNode* &rear, int &x){
ListNode* s = nullptr;
if(rear->next==rear) return false;
else
{
s = rear->next->next;
rear->next->next = s->next;
x = s->val;
if(s==rear) rear = rear->next;
delete s;
s = nullptr;
return true;
}
}
【习题】
【解析】
约定
f
r
o
n
t
front
front 指向队头元素的前一位置,
r
e
a
r
rear
rear 指向队尾元素,定义满足
f
r
o
n
t
=
=
r
e
a
r
front == rear
front==rear 时队空。从队尾删元素时,
r
e
a
r
rear
rear 向着下标减小的方向走,从队头插入元素时,
f
r
o
n
t
front
front 向着下标减小的方向走。因此,当满足
r
e
a
r
=
(
f
r
o
n
t
−
1
+
m
a
x
s
i
z
e
)
%
m
a
x
s
i
z
e
rear=(front-1+maxsize)\%maxsize
rear=(front−1+maxsize)%maxsize 时队满。
typedef struct
{
int nums[maxsize];
int front, rear;
}cycque
bool Deque(cycque &q, int &x){ // 队尾删除
if(q.front==q.rear) return false;
x = q.nums[q.rear];
q.rear = (rear-1+maxsize)%masize;
return true;
}
bool Enque(cycque &q, int x){ // 队头插入
if(q.rear==(q.front-1+maxsize)%maxsize) return 0;
q.nums[q.front] = x;
q.front = (q.front-1+maxsize)%maxsize;
return true;
}
注意,算法中频繁使用了 f r o n t = ( f r o n t − 1 + m a x s i z e ) % m a x s i z e front=(front-1+maxsize)\%maxsize front=(front−1+maxsize)%maxsize 之类的操作,如果把这一语句放到循环中,那么 f r o n t front front 指针将沿着 m a x s i z e − 1 , m a x s i z e − 2 , ⋯ , 2 , 1 , 0 , m a x s i z e − 1 , m a x s i z e − 2 , ⋯ maxsize-1,maxsize-2,\cdots,2, 1, 0, maxsize-1, maxsize-2,\cdots maxsize−1,maxsize−2,⋯,2,1,0,maxsize−1,maxsize−2,⋯ 的无限循环走下去,刚好和 f r o n t = ( f r o n t + 1 ) % m a x s i z e front=(front+1)\%maxsize front=(front+1)%maxsize 实现的效果相反
【习题】
【解析】
typedef struct
{
int nums[maxsize];
int front, rear;
int tag
}cycque
void Init(cycque &q){
q.front = q.rear = 0;
q.tag = 0;
}
bool isEmpty(cycque q){
if(q.front==q.rear&&!tag) return true;
return false;
}
bool isFull(cycque q){
if(q.front==q.rear&&tag) return true;
return false;
}
bool Enque(cycque &q, int x){
if(isFull(q)) return false;
q.rear = (q.rear+1)%maxsize;
q.nums[q.rear] = x;
q.tag = 1;
return true;
}
bool Deque(cycque &q, int &x){
if(isEmpty(q)) return false;
q.front = (q.front+1)%maxsize;
x = q.nums[front];
q.tag = 0;
return true;
}
注意,对于 t a g tag tag 的赋值。初始的时候一定是 0,插入成功后置为 1,删除成功后置为 0,因为之后再插入操作之后队列才有可能满,只有在删除操作后,队列才有可能空
【习题】
【解析】
int Solution(int n){
int res = 0, tmp = 0;
int st[maxsize], top = -1;
while(n)
{
tmp = n%2;
n /= 2;
st[++top] = tmp;
}
while(top!=-1)
{
tmp = st[top];
top--;
res = res*10+tmp;
}
return res;
}
【习题】
【解析】
// 递归
float Solution(flaot A, float p, float e){
if(fabs(p*p-A)<e) return p;
else return Solution(A, (p+A/p)/2, e);
}
// 非递归
float Solution(flaot A, float p, float e){
while(fabs(p*p-A)>=e) p = (p+A/p)/2;
return p;
}
【习题】
【解析】
S
o
l
u
t
i
o
n
(
s
t
r
,
k
,
n
)
Solution(str, k, n)
Solution(str,k,n) 求
s
t
r
[
k
]
−
s
t
r
[
n
]
str[k]-str[n]
str[k]−str[n] 的全排列。那么,如果
S
o
l
u
i
o
t
n
(
s
t
r
,
k
−
1
,
n
)
Soluiotn(str, k-1, n)
Soluiotn(str,k−1,n) 可求的话,对于
s
t
r
[
k
]
str[k]
str[k],就可取
s
t
r
[
0
]
−
s
t
r
[
k
]
str[0]-str[k]
str[0]−str[k] 中的任意值,再组合
S
o
l
u
t
i
o
n
(
s
t
r
,
k
−
1
,
n
)
Solution(str, k-1, n)
Solution(str,k−1,n),就得到
S
o
l
u
t
i
o
n
(
s
t
r
,
k
,
n
)
Solution(str, k, n)
Solution(str,k,n)
void Solution(vector<char> &str, int k, int n){
char tmp;
if(k==0)
for(int i=0;i<n-1;i++) cout << str[i];
else
{
for(int i=0;i<=k;i++)
{
tmp = str[k];
str[k] = str[i];
str[i] = tmp;
Solution(str, k-1, n);
tmp = str[i];
str[i] = str[k];
str[k] = tmp;
}
}
}
树相关
【2014真题】
【解析】
利用层遍历记录每个结点当前所处的层,如果遇到叶结点就计算带权路径长度
int Solution(TreeNode *root){
if(!root) return 0;
int wpl = 0, deep = 0;
TreeNode *lastnode, *newlastnode;
deque<TreeeNode*> q;
q.push_back(root);
while(!q.empty())
{
TreeNode *tmp = q.front();
q.pop_front();
if(!tmp->left && !tmp->right) wpl += deep*tmp->weight;
if(tmp->left)
{
q.push_back(tmp->left);
newlastnode = tmp->left;
}
if(tmp->right)
{
q.push_back(tmp->right);
newlastnode = tmp->right;
}
if(tmp = lastnode)
{
lastnode = newlastnode;
deep += 1;
}
}
return wpl;
}
除开本题所讨论的内容,有一个很关键的部分就是如何在层遍历中求结点所处的层次
【2017真题】
【解析】
添加括号时注意根结点和叶子结点时不填括号的
void helper(BTree *root, int deep){
if(!root) return;
else if(!root->left && !root->right) cout << root->data;
else
{
if(deep>1) cout << "(";
helper(root->left, deep+1);
cout << root->data;
helper(root->right, deep+1);
if(deep>1) cout << ")";
}
}
void Solution(BTree *root){
helper(root, 1);
}
【习题】
【解析】声明一点,把类型 char 改为 类型 int,出题的是脑子吃多了弄个啥的字符类型
TreeNode* CreateBTree(vextor<int> pre, vector<int> in, int l1, int r1, int l2, int r2){
TreeNode* root = new TreeNode(-1);
int i;
if(l1>r1) return nullptr;
root->left = root->right = nullptr;
for(i=l2;i<=r2;i++)
if(in[i]=pre[l1]) break;
root->val = in[i];
root->left = CreateBTree(pre, in, l1+1, l1+i-l2, l2, i-1);
root->right = CreateBTree(pre, in, l1+i-l2+1, r1, i+1, r2);
return root;
}
【习题】
【解析】
int Solution(TreeNode* root) {
int n_left, n_right;
if(!root) return 0;
n_left = Solution(root-left);
n_right = Solution(root->right);
return n_left+n_right+1;
}
【习题】
【解析】
int Solution(TreeNode* root) {
int n_left, n_right;
if(!root) return 0;
if(!root->left && !root->right) return 1;
n_left = Solution(root->left);
n_right = Solution(root->right);
return n_left+n_right;
}
【习题】
【解析】
先给个图,看明白题干在说啥玩意的
可以明确的一点,不管哪种遍历方式叶子结点的相对顺序都不会改变,所以我们就只需要在先序遍历中判断叶子结点,再单独处理就万事大吉
void Solution(TreeNode* root, TreeNode* &head, TreeNode* &tail){
if(!root)
{
if(!root->left && !root->right)
{
if(!head) // 第一个叶子结点
{
head = root;
tail = root;
}
else
{
tail->right = root;
tail = root;
}
}
Soluiton(root->left, head, tail);
Solution(root->right, head, tail);
}
}
【习题】
【解析】
struct TreeNode{
int val;
TreeNode* left;
TreeNode* right;
TreeNode* parent;
TreeNode(int x) : val(x), left(nullptr), right(nullptr), parent(nullptr) {}
};
void FindParent(TreeNode* cur, TreeNode* par){
if(cur)
{
cur->parent = par;
par = cur;
FindParent(cur->left, par);
FindParent(cur->right, par);
}
}
void PrintPath(TreeNode* cur){
while(cur)
{
cout << cur->val << " " << endl;
cur = cur->parent;
}
}
void PrintAllPath(TreeNode* root){
if(root)
{
PrintPath(root);
PrintAllPath(root->left);
PrintAllPath(root->right);
}
}
【习题】
【解析】
已知先序遍历的结果,则序列的第一个元素即为根节点,将除去第一个元素后的序列分成前后长度相等的两半,前一半为左子树上的结点,后一半为右子树上的结点。只需要将根节点移动到整个序列的末尾,然后分别递归地去处理两个子序列就行
void Solution(vector<int> pre, int l1, int r1, vector<int> &post, int l2, int r2){
if(l1<=r1)
{
post[r2] = pre[l1]; // 将 pre 的第一个元素放在 post 的最后
Solution(pre, l1+1, (l1+1+r1)/2, post, l2, (l2+r2-1)/2); // 处理 pre 中的前一半子序列,存在 post 的前一半中
Solution(pre, (l1+1+r1)/2, r1, post, (l2+r2-1)/2+1, r2-1); // 处理 pre 中的后一半子序列,存在 post 的后一半中
}
}
【习题】
【解析】
int layer = 0;
int Solution(TreeNode* root, int x){
if(root)
{
if(root->val==x) return layer;
layer++;
Solution(root->left, x);
Solution(root->right, x);
layer--;
}
}
这里对 layer--
解释一下:如果按照层次遍历的话,当然就没有这个破事。但是上面代码中采用的是先序遍历的方式,所以我们看下面这图展示指针在遍历整个树时的行走路线
很清楚地可以看到,在访问当前结点的左子树的时候指针向下移动层数
+
1
+1
+1,当访问完当前结点右子树所有结点后,指针向上移动层数自然就应该
−
1
-1
−1
【习题】
【解析】
void Solution(TreeNode* root){
if(root)
{
visit(root);
Solution(root->left);
visit(root);
Solution(root->right);
}
}
【习题】
【解析】
1)沿着结点 t 的右子树一直往下走,直到遇到右指针为右线索的结点为止,该节点即为所求结点
TBTNode* inLast(TBTNode* t){
TBTNode* p = t;
while(p && !p->rtag) p = p->right;
return p;
}
2)若结点 t 有左线索,则其左线索所指结点即为其中序的前驱结点;反之,则其左子树的最后一个结点为其中序的前驱结点
TBTNode* inPrior(TBTNode* t){
TBTNode* p = t->left;
if(p && !p->left) p =p->left;
return p;
}
TBTNode* treNext(TBTNode* root){
TBTNode* p = t;
if(!t->ltag) p = t->left;
else if(!t->rtag) p = t->right;
else
{
while(p && p->rtag) p = p->right;
if(p) p = p->right;
}
return p;
}
【习题】
【解析】
stack<int> st;
void PrintAllPath(TreeNode* root){
int tmp;
if(root)
{
st.push(root->val);
if(!root->left && !root->right)
{
while(!st.empty())
{
tmp = st.top();
st.pop();
count << tmp;
}
}
PrintAllPath(root->left);
PrintAllPath(root->right);
st.pop();
}
}
图相关
【2014真题】
【解析】
(1)题中给出的是一个简单的网络拓扑图,可以抽象为无向图
(2)链式存储结构如下图所示
对应代码定义为
struct LinkNode{
unsigned int ID, IP;
};
struct NetNode{
unsigned int Prefix, Mask;
};
struct ArcNode{
int flag;
LinkNode Lnode;
NetNode Nnode;
unsigned int Metric;
ArcNode *next;
};
struct Head{
unsigned int RounterID;
ArcNode *link;
Head *next;
};
(3)计算结果 如下表所示
【习题】
【解析】在不带权的无向图中,BFS 体现了从图中某个顶点开始,以由近向远层层扩展的方式遍历图中结点的过程,故只需要在 BFS 中返回最后访问的一个顶点即可
vector<int> visited(maxsize, 0);
int Solution(AGraph* G, int v){
ArcNode *p;
deque<int> dq;
int tmp;
Visit(v);
visited[v] = 1;
dq.push_back(v);
while(!dq.empty())
{
tmp = dq.front();
dp.pop_front();
p = G->adjlist[tmp].firstarc;
while(p)
{
if(!visited[p->adjvex])
{
Visit(p->adjvex);
visited[p->adjvex] = 1;
dq.push_back(p->adjvex);
}
p = p->next;
}
}
return tmp;
}
【习题】
【解析】要求
∣
E
∣
=
∣
V
∣
−
1
|E|=|V|-1
∣E∣=∣V∣−1 ,题中还应该假设无向图的顶点数和边数是未知的
void helper_DFS(AGraph* G, int v, int &vcnt, int &ecnt){
ArcNode* p;
visited[v] = 1;
Visit(v);
vnt++;
p = G->adjlist[v].first;
while(p)
{
ecnt++;
if(!visited[p->adjvex])
{
Solution(G, p->adjvex, vcnt, ecnt);
p = p->next;
}
}
}
int Solution(AGraph* G){
int vnt = 0, ecnt = 0;
vector<int> visited(G->n, 0);
helper_DFS(G, 0, vcnt, ecnt);
if((vcnt==G->n)&& (ecnt/2==(G->n-1)) return 1; // 注意前面代码在算边的时候有重复计数
else return 0;
}
【习题】
【分析】
int Solution(AGraph* G, int i, int j){
vector<int> visited(G->n, 0);
DFS(G, i);
if(visited[j]) return 1;
else return 0;
}
【习题】
【解析】
bool hasCyc(AGraph* g, int v, vector<bool> visited){
bool flag;
ArcNode* p;
visited[v] = true;
for(p=g->adjList[v].fist;p!=nullptr;p=p->next)
{
if(visited[p->adjV]==true) return true;
else flag = hasCyc(g, p->adjV,visited);
if(flag) return true;
visited[p->adjV] = false;
return false;
}
}
【习题】
【解析】
void Solution(<vector<vector<int>> &g1, AGraph g2){
ArcNode* p = nulptr;
memset(g1, 0, sizeof(g1));
for(int i=0;i<g1.szie();i++)
{
p = g2.adjList[i].fisrt;
while(p)
{
g1[i][p->adjvex] = 1;
p = p->next;
}
}
}
【习题】
【解析】
int Solution(AGraph* g, int k){
ArcNode* p = nullptr;
int sum;
for(int i=0;i<g->n;i++)
{
p = g->adjList[i].first;
while(p)
{
if(p->adjvex==k)
{
sum++;
break;
}
p = p->next ;
}
}
return sum;
}
【习题】
【解析】
void DFS(AGraph* g, int v){
ArcNode* p;
vector<int> st;
vector<int> visited(maxsize);
Visit(v);
visited[v] = 1;
st.push_back(v);
while(st)
{
int k = st.back();
p = g->adjList[k].fisrt;
while(p && visited[v->adjvex]) p = p->next;
if(!p) st.pop_back();
else
{
Visit(p->adjvex);
visited[p->adjvex] = 1;
st.push_back(p->adjvex);
}
}
}
【习题】
【解析】最小生成树算法,因为有些道路已经存在了,所以只在此基础上继续建立,这里采用克鲁斯卡尔算法
int getRoot(int a, vector<int> set){ // 找并查集中根节点的函数
while(a!=set[a]) a = set[a];
return a;
}
int LowCost(vector<Road> road){
int a, b;
int mmin = 0;
for(int i=0;i<N;i++)
set[i] = i; // 初始化并查集,各村庄是孤立的,因此自己就是根节点
for(int i=0;i<M;i++)
{
if(road[i].is) // 将已经有路相连的村庄合并为一个集合
{
a = getRoot(road[i].a, set);
b = getRoot(road[i].b, set);
set[a] = b;
}
}
// 对 road 中的 M 条 道路按照花费进行排序
sort(road, M);
// 从 road 中逐个调出应修的道路
for(int i=0;i<M;i++)
{
a=getRoot(road[i].a, set);
b=getRoot(road[i].b, set);
if(!road[i].is && a!=b) // 当 a、b 不属于一个集合,并且 a、b 间没有道路时,将 a、b 并入一个集合,并记录下修建 a、b 间道路所需的花费
{
set[a] = b;
mmin += road[i].cost;
}
}
return mmin;
}
【习题】
【解析】
bool BFS(AGraph* g, int v, int u){
ArcNode* p = nullptr;
vector<int> qu;
vector<int> visited(maxsize);
qu.push_back(v);
visited[v] = 1;
while(visited.size())
{
int tmp = qu.pop(0);
if(tmp==u) return true;
p = G->adjList[tmp].first;
while(p)
{
if(!visited[p->adjvex])
{
qu.push_back(p->adjvex);
visited[p->adjvex] = 1;
}
p = p->next;
}
}
return false;
}
时间复杂度为 O ( n + e ) \mathcal O(n+e) O(n+e)
【习题】
【解析】
vector<int> visited(maxsize);
int sum = 0;
void DFS(AGraph* g, int v){
ArcNode* p = g->adjList[v].first;
visited[v] = 1;
sum++;
while(p)
{
if(!visited[p->adjvex]) DFS(g, p->adjvex);
p = p->next;
}
}
void PrintAllRoot(AGraph* g){
for(int i=0;i<g->n;i++)
{
sum = 0;
memset(visited, 0, sizeof(visited));
DFS(g, i);
if(sum==g->n) cout << i << endl;
}
}
排序相关
【2016年真题】
【解析】
其实题意翻译过来就是,把最小的 n / 2 n/2 n/2 个元素归为 A 1 A_1 A1,把剩下的所有元素归为 A 2 A_2 A2,虽然官方题解中给出了一种时间复杂度为 O ( n ) O(n) O(n) 的方法,因为按照快排的思路,其实是不需要对全序列进行排序的
但是,从答题的角度(毕竟就是背一个排序算法上去嘛),我还是选择对全序列应用快速排序,时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),空间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n),根据评分规则,官方 O ( n ) O(n) O(n) 的方法评分 13 分, O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) 评分 11 分,相差无几
int partition(vector<int> &nums, int l, int r){
int pivot = l, key = nums[l];
for(int i=l;i<=r;i++)
if(nums[i]<key) swap(nums[i], nums[++pivot]);
swap(nums[pivot], nums[l]);
return pivot;
}
void quicksort(vector<int> &nums, int l, int r){
if(l<r)
{
int pos = partition(nums, l, r);
quicksort(nums, l, pos-1);
quicksort(nums, pos+1, r);
}
}
int Solution(vector<int> &nums){
int n = nums.size(), s1=0, s2=0;
quicksort(nums, 0, n-1);
for(int i=0;i<n;i++)
{
if(i<n/2) s1 = s1+nums[i];
else s2 = s2+nums[i];
}
return s2-s1;
}
【习题】
【解析】
void ContSort(vector<int> A, vector<int> &B, int n) {
int cnt;
for(int i=0;i<n;i++)
{
cnt = 0;
for(j=0;i<n;j++)
if(A[j]<A[i]) cnt++;
B[cnt] = A[i];
}
}
比较次数为 n 2 n^2 n2,简单选择排序比这种计数排序好,因为简单选择排序的空间复杂度是常量级的
【习题】
【解析】
我们先给出正确的快速排序
void QuickSort(vector<int> R, int low, int high){
int tmp;
int i = low, j = high;
if(low<high)
{
tmp = R[low];
while(i<j)
{
while(j>i && R[j]>=tmp) j--;
if(i<j)
{
R[i] = R[j];
i++
}
while(i<j && R[i]<tmp) i++;
if(i<j)
{
R[j] = R[i];
j--;
}
}
R[i] = tmp;
QuickSort(R, low, i-1);
QuickSort(R, i+1, high);
}
}
可以发现题目中的代码和正误的代码不同在 tmp=low
及 R[j]>=R[tmp]
,也就是说题中的快排代码保存的是关键字的下标,由于后面的比较移动过程中可能会改变 tmp
下标所指位置上的关键字,造成 R[i]=R[tmp]
赋值错误,从而引起排序失败
【习题】
【解析】双向冒泡排序算法也称为鸡尾酒排序法
void cocktailsort(vector<int>& nums)
{
int l = 0, r = (int)nums.size()-1;
while(l<r)
{
for(int i=l;i<r;i++)
if(nums[i]>num[i+1]) swap(nums[i], nums[i+1]);
r--;
for(int i=r;i>l;i--)
if(nums[i]<nums[i-1]) swap(nums[i], nums[i-1]);
l++;
}
}
【习题】
【解析】
void Solution(vector<int> &nums){
int left = 0, right = nums.size()-1;
while(left<right)
{
while(left<right && nums[left]<0) left++;
tmp = nums[left];
while(left<right && nums[right]>0) right--;
swap(nums[left], nums[right]);
left++;
right--;
}
}
时间复杂度为 O ( n ) \mathcal O(n) O(n),空间复杂度为 O ( 1 ) \mathcal O(1) O(1)
【习题】
【解析】
void Slution(vector<int> A, vector<int> &B){
for(int i=0;i<A.size();i++)
B[A[i]] = A[i];
}
时间复杂度为 O ( n ) \mathcal O(n) O(n),时间复杂度为 O ( n ) \mathcal O(n) O(n)
查找相关
习题】
【解析】
但是怎么理解这句话呢?
struct TreeNode{
int val;
int lsize;
TreeNode* left;
TreeNode* right;
};
TreeNode* Solution(TreeNode* root, int k){
if(!root || k<1) return nullptr;
if(root->lsize==k) return root;
if(root->lsize>k) return Solution(root->left, k);
else return Solution(root->right, k);
}
【习题】
【解析】
struct TreeNode{
int val;
int count;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), count(1), left(nullptr), right(nullptr) {}
};
void Solution(TreeNode* &root, TreeNode* &pre, int x){ // pre 指向 root 的父结点
TreeNode* p = root;
while(!p)
{
if(p->val!=x)
{
pre = p;
if(x<p->val) p = p->left;
else p = p->right;
}
else
{
(p->count)++;
return;
}
}
TreeNode* tmp = new TreeNode(x);
if(!pre) root = tmp;
else if(x<pr->val) pr->left = tmp;
else pre->right = tmp;
}
【习题】
【解析】先说明一下理论部分,对序列
S
S
S,如果从其能依据给定的值生成两个子序列
S
1
、
S
2
S_1、S2
S1、S2,
S
1
S_1
S1 单调递减且每个元素均小于给定值,
S
2
S_2
S2 单调递增且每个元素均大于给定值,则我们说
S
S
S 是二叉排序树的一个查找序列
void Split(vector<int> &S, vector<int> &S1, vector<int> &S2){
int i = 0;
S.push_back('#'); // 结束标志位
while(S[i]!='#')
{
while(i<S.size()-1 && S[i]<S[i+1])
{
S1.push_back(S[i]);
i++;
}
while(i<S.size()-1 && S[i]>S[i+1])
{
S2.push_back(S[i]);
i++;
}
}
}
bool Judge(vector<int> S1, vector<int> S2, int x){
bool flag = true;
int i = 0;
for(int i=0;i<S1.size()-1 && flag;i++)
if(S1[i]>x) flag = false;
for(int i=0;i<S2.size()-1 && flag;i++)
if(S2[i]<x) flag = false;
return flag;
}
bool Solution(vector<int> &S, int x){ // x 是给定的查找值
vector<int> S1;
vector<int> S2;
Split(S, S1, S2);
return Judge(S1, S2, x);
}
【习题】
【解析】对给定的二叉树进行中序遍历,如果能保证前一个值不大于后一个值,那么该二叉树就是一棵二叉排序树
int pre = INT_MIN;
bool Solution(TreeNode* root){
bool flag_left = flag_right = false;
if(!root) return true;
flag_left = Solution(root->left);
if(!flag_left || pre>root->val) return false;
pre = root->val;
flag_right = Solution(root->right);
return flag_right;
}
本题的实质是对排序树的中序遍历,故时间复杂度为 O ( n ) \mathcal O(n) O(n),递归遍历,空间复杂度为 O ( l o g 2 n ) \mathcal O(log_2n) O(log2n)