【习题】
【解析】C,有序表指出了表中数据时根据一定逻辑结构顺序排列的,值一种逻辑结构
【习题】
【解析】
数组和单链表的处理是一样的,这里给出 LeetCode 21 - 合并两个有序链表 迭代的解法,时间复杂度是 O ( m + n ) O(m+n) O(m+n),空间复杂度是 O ( 1 ) O(1) O(1)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode newhead(0);
ListNode *p = &newhead;
while(l1&&l2)
{
if(l1->val > l2->val) swap(l1, l2);
p->next = l1;
l1 = l1->next;
p = p->next;
}
p->next = l1?l1:l2;
return newhead.next;
}
};
void 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;
}
}
}
【习题】
【解析】C
【回顾】
【习题】
【解析】
【习题】
【解析】
【习题】
【解析】
【习题】
【解析】
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[i] = L.data[j];
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;
}
void solution(ListNode* head){
ListNode* pre = nullptre;
ListNode* cur = head ->next;
ListNode* tmp = nullptr;
while(cur!=nullptr)
{
tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
head->next = pre;
}
void solution(ListNode* A, ListNode* &B){
ListNode *p, *q, *r;
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;
}
}
数组找最小值不是难题,一个循环加一个辅助变量就可以找到最小的元素了。但是这道题有一个限制的地方就在于没有两个变量可用。于是, i i i 就担负起了一个变量两个任务的责任,我们取 i i i 为一个两位的数字,其十位数字表示循环控制变量,个位数字表示当前位置的元素值
void solution(vector<int>& A, int& i){
i = A[0];
while(i/10<=N-1)
{
if(i%10>A[i/10])
{
i -= i%10;
i += A[i/10];
}
i += 10;
}
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 次,平均一下 n − 1 + n / 2 − 1 / 2 = 3 n / 2 − 3 / 2 < 3 n / 2 n-1+n/2-1/2=3n/2-3/2<3n/2 n−1+n/2−1/2=3n/2−3/2<3n/2,所以就一趟遍历,没那么多事
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;
}
【习题】
【解析】B
【习题】
【解析】题目有误,最好、最坏情况下都是 O ( m + n ) O(m+n) O(m+n)
一句话解决的事,剑指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;
cout << "n" << n << endl;
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 的时候,我们就需要重新选取我们的假定主元素重复上面的过程
【双端栈】
若栈采用顺序存储方式存储,现两栈共享空间,两栈栈底分别在顺序空间的两端,则栈满的条件是 t o p [ 1 ] + 1 = t o p [ 2 ] top[1]+1=top[2] top[1]+1=top[2]
【中缀表达式转后缀表达式】
【循环队列】
队空: f r o n t = = r e a r front == rear front==rear (少用一个元素空间,约定队列头指针在尾指针的下一位置为满)
队满: ( r e a r + 1 ) % m a x s i z e = = f r o n t (rear+1)\%maxsize == front (rear+1)%maxsize==front
入队: r e a r = ( r e a r + 1 ) % m a x s i z e rear = (rear+1)\%maxsize rear=(rear+1)%maxsize
出队: 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
求长度: ( r e a r − f r o n t + m a x s i z e ) % m a x s i z e (rear-front+maxsize)\%maxsize (rear−front+maxsize)%maxsize
bool isEmpty(cycque q){
if(q.front==q.rear) return true;
return false;
}
bool isFull(cycque q){
if(a.front==(q.rear+1)%maxsize) return true;
return false;
}
bool Enque(cycque &q, int x){
if(isFull(q)) return false;
q.nums[q.rear] = x;
q.rear = (q.rear+1)%maxsize;
return true;
}
bool Deque(cycque &q, int &x){
if(isEmpty(q)) return false;
x = q.nums[front];
q.front = (q.front+1)%maxsize;
return true;
}
【习题】对于链队,在进行出队操作时(D)头、尾指针可能都要修改
【习题】用不带头节点的单链表存储队列时,其队头指针指向对头结点,其队尾指针指向队尾节点,则在进行出队操作时(D)头、尾指针可能都要修改
【解析】同上题
【习题】
【解析】D,因为链栈在链表的头部进行操作,需要很方便找到链表开始结点的前驱,而只有表头指针、没有表尾指针的循环单链表找前驱的时间复杂度是 O ( n ) O(n) O(n)
【习题】一栈的进栈序列是p1, p2, …, pn,输出序列为1, 2, 3, …, n,若p3=1,则p1()答:不可能是 2
【解析】p3 = 1 且1是第一个出栈元素,p1,p2,p3这三个元素连续进栈,第二个出栈的是2,此时栈顶是p2,故只有p2, …, pn可能为2
【习题】一个栈如展序列为1,2,3,…,n,出栈序列为p1,p2,p3,…,pn,若p2=3,则p3可取的个数是()答:n-1
【解析】除3之外,其他值是均有可能取到的
【习题】用单链表,含头节点表示的队列的队头在链表的()位置。 答:链尾、链中
【解析】当队列只有一个元素时,头指针指向尾结点;当队列中元素大于一个时,头指针指向链中(链中位置是指除去头节点、尾结点的位置)
【习题】某队列,两端可入队,出队仅允许在一端,a,b,c,d,e,依次入队,则不可能得到的顺序是( )
A:bacde
B:dbace
C:dbcae
D:ecbad
【习题】已知循环队列存储在一维数组A[0,1,…,n-1]中,且队列非空时front和rear分别指向队头和队尾元素。若初始时队列为空,且要求第1个进入队列的元素存储在A[0],则初始时front和rear的值为
A) 0,0
B) 0,n-1
C) n-1,0
D) n-1,n-1
【解析】B,注意队列非空时front和rear分别指向队头和队尾元素,如果认为是D选择,则在进入一个元素后front=n-1,rear=0,这时队列是两个元素
【习题】下列叙述中正确的是( )。
A) 在栈中,栈顶指针的动态变化决定栈中元素的个数
B) 在循环队列中,队尾指针的动态变化决定队列的长度
C) 在循环链表中,头指针和链尾指针的动态变化决定链表的长度
D) 在线性链表中,头指针和链尾指针的动态变化决定链表的长度
【解析】A
在栈中,栈底指针保持不变,有元素入栈,栈顶指针增加,有元素出栈,栈顶指针减少
在循环队列中,队头指针和队尾指针的动态变化决定队列的长度
无论在循环链表还是线性链表中,要插入或删除元素,只需要改变相应位置的结点指针即可,头指针和尾指针无法决定链表长度
【习题】
【解析】
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;
}
【习题】写出下列中缀表达式的后缀形式
( ! ( A & & ( ! ( ( B < C ) ∣ ∣ ( C > D ) ) ) ) ) ∣ ∣ ( C < E ) (!(A\&\&(!((B
注:单目运算符, ! A !A !A 的后缀表达式是 A ! A! A!
【解析】 A B C < C D > ∣ ∣ ! & & ! C E < ∣ ∣ ABC
【习题】
【解析】
知道尾指针后,实现元素入队,则直接在链表尾插入就行。而实现出队,而需要根据尾指针找到头结点和开始节点,然后进行删除。要注意的是,尾指针应始终指向终端结点,并且当删除结点后队列为空时,必须特殊处理。
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;
}
【习题】
【解析】这里,我们让普通元素(即除了括号、引号的元素)不进栈
int Solution(char f[]){
stack<char> st;
char ch;
char* p = f;
while(*p!='\0')
{
if(*p==39)
{
++p;
while(*p!=39) ++p;
++p;
}
else if(*p==34)
{
++p;
while(*p!=34) ++p;
++p;
}
else
{
switch(*p)
{
case '{':
case '[':
case '(': st.push(*p); break;
case '}': if(s.top()=='{') st.pop(); else return 0; break;
case ']': if(s.top()==']') st.pop(); else return 0; break;
case ')': if(s.top()==')') st.pop(); else return 0; break;
}
}
++p;
}
if(st.empty()) return 1;
else return 0;
}
// 递归
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 [ 0 ] − s t r [ k ] str[0]-str[k] str[0]−str[k] 的全排列。那么,如果 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;
}
}
}
【二叉树结构的定义】
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
【习题】
【解析】声明一点,把类型 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 = s->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;
}
【习题】
根据使用频率为 5 个字符设计的哈夫曼编码不可能是( )。
A 000,001,010,011,1
B 0000,0001,001,01,1
C 000,001,01,10,11
D 00,100,101,110,111
【解析】
D,哈夫曼树的节点要么是叶子节点,要么是度为2的节点,不可能出现度为1的节点
【习题】
【解析】C
哈夫曼编码为前缀码,即编码中任何一个序列都不是另一个序列的前缀
【习题】
设一棵三叉树中有 2 个度数为 1 的结点, 2 个度数为 2 的结点, 2 个度数为 3 的结点,则该三叉树中有( )个度数为 0 的结点。
【解析】
(1) 三叉树的结点总数为: N = n 0 + n 1 + n 2 + n 3 N=n_{0}+n_{1}+n_{2}+n_{3} N=n0+n1+n2+n3
(2) i i i 度结点有 i i i 个孩子,根结点不是任何结点的孩子,结点总数为: N = n 1 + 2 n 2 + 3 n 3 + 1 N=n_{1}+2n_{2}+3n_{3}+1 N=n1+2n2+3n3+1
得到: n 0 = n 2 + 2 n 3 + 1 n_{0}=n_{2}+2n_{3}+1 n0=n2+2n3+1
【习题】
【解析】D,就对应看成中序遍历结果和后续遍历结果就行了
【习题】
【解析】A,根节点 p p p 也属于第一棵树的结点
【习题】
【解析】B
由树的度的定义可知,树中各个结点度的最大值为树的度,单个结点也算是一棵树。那么对于一个只有一个节点的二叉树,其度为 0
【习题】
【解析】B
树的先序遍历对应于二叉树的先序遍历,树的后序遍历对应于二叉树的中序遍历
【习题】
【解析】C
要交换所有分支结点左、右子树的位置,可以递归地进行如下的操作:先递归交换左子树中的所有分支结点,再递归地交换右子树中的所有分支结点,最后对根结点交换其所有子树的位置
【习题】
深度为 k k k 的完全二叉树中最少有()个结点。
A 2 ( k − 1 ) − 1 2^{(k-1)}-1 2(k−1)−1
B 2 ( k − 1 ) 2^{(k-1)} 2(k−1)
C 2 ( k − 1 ) + 1 2^{(k-1)}+1 2(k−1)+1
D 2 k − 1 2^k-1 2k−1
【解析】B
完全二叉树的最少情况是最后一层只有最左边有一个叶子结点,那么上面的 k-1 层数满二叉树节点数为 2 ( k − 1 ) − 1 2^{(k-1)}-1 2(k−1)−1,因此完全二叉树的节点数是 2 ( k − 1 ) − 1 + 1 = 2 ( k − 1 ) 2^{(k-1)}-1+1=2^{(k-1)} 2(k−1)−1+1=2(k−1)
多提一句,深度为 k k k 的完全二叉树中最多有 2 k − 1 2^k-1 2k−1 个结点
【习题】
【解析】B
不论是哪一种遍历方式,所得遍历序列中叶子结点的相对位置都是不变的
【习题】
【解析】C
当每层只有一个结点是我们能得到最大高度,当这颗二叉树是满二叉树时高度最小
【习题】
【解析】C
【习题】
【解析】
【习题】
已知一棵有 2011 个结点的树,其叶结点个数为 116 ,该树对应的二叉树中无右孩子的结点个数是 ( )
【解析】1896
普通树转化为二叉树后,度为1的结点只有左孩子而无右孩子,考虑如下特殊情况:
也就是最后一层上面的1895个结点再加最后一个叶子节点没有右孩子
【习题】
若一棵二叉树的前序遍历序列为a, e, b, d, c,后序遍历序列为 b, c, d, e, a,则根结点的孩子结点( )。
A 只有 e
B 有 e、 b
C 有 e、 c
D 无法确定
【解析】A
二叉树遍历的性质:
题中问的是根结点的孩子结点,前序+后序是可以确定的,但是一般情况下不能确定整个二叉树。当前序关系为 XY 而后序关系为 YX 时,则 X 为 Y 的祖先
如,该题:
【习题】
在任意一棵非空二叉排序树T1中, 删除某结点v之后形成二叉排序树 T2,再将v 插入T2形成二叉排序树T3。下列关于T1与T3的叙述中,正确的是( )。
I.若 v 是 T1的叶结点,则 T1 与 T3 不同
II. 若 v 是 T1的叶结点,则 T1与 T3相同
III.若 v 不是 T1 的叶结点,则 T1 与 T3 不同
IV.若v 不是 T1 的叶结点,则 T1 与 T3 相同
【解析】II 和 III 正确
【习题】
【解析】B
对于结点数大于等于 2 的待处理序列都可以构造哈夫曼二叉树,但是不一定能构成哈夫曼 n 叉树。当发现无法构造时,需要补上权值为 0 的结点。故本题序列构造出的哈夫曼 3 叉数为
【习题】
【解析】B
高度为 6 的平衡二叉树最少有 20 个结点。每个非叶子结点的平衡因子均为 1,即暗示了这种最少结点的极端情况,因为增加一个结点可以使得某个结点的平衡因子变为 0,而不会破坏平衡性
【习题】
【解析】A
根据 A)选项构造的二叉排序树为
91 的左子树出现了 94(>91)
【习题】
【解析】B
I 和 II 都很容易明白。重点说 III ,如下图就解释清楚了 III
但是,我当时给 III 想到下面的例子,认为 III 是正确的,是错误理解了 u 的父结点和 v 的父结点是指原森林中 u 的父结点和 v 的父结点,而不是二叉树中的
【习题】
【解析】
1)二叉树的第 d d d 层最多有 2 d − 1 2^{d-1} 2d−1 个结点,故深度为 d d d 的不同完全二叉树
2)深度为 d d d 的满二叉树就只有一棵
【习题】
【解析】
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);
FindParent(cur->right);
}
}
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();
}
}
【习题】
【解析】
1)满 K K K 叉树各层的结点构成一个首相为 1 1 1,公比为 K K K 的等比数列,各层结点数为 K h − 1 K^{h-1} Kh−1
2)每层上均有 K h − 1 K^{h-1} Kh−1 个节点,从根节点开始编号为 1 1 1,则结点 i i i 从右向左数第 2 2 2 个孩子的结点编号为 K × i K\times i K×i,于是,设 n n n 为结点 i i i 的子女,则关系式 ( i − 1 ) × K + 2 ⩽ n ⩽ i × K + 1 (i-1)\times K+2\leqslant n \leqslant i\times K +1 (i−1)×K+2⩽n⩽i×K+1,因 i i i 是整数,故结点 n n n 的双亲结点 i i i 的编号为 ⌊ n − 2 k ⌋ + 1 \left \lfloor \frac{n-2}{k} \right \rfloor+1 ⌊kn−2⌋+1
3)结点 n n n 的前一结点的最右边孩子的编号为 K × ( n − 1 ) + 1 K\times(n-1)+1 K×(n−1)+1,故 结点 n n n 的第 i i i 个孩子的编号为 K × ( n − 1 ) + 1 + i K\times(n-1)+1+i K×(n−1)+1+i
4)结点 n n n 要能有右兄弟,则它不是双亲从右向左数的第一个子女,即 ( n − 1 ) m o d K ≠ 0 (n-1)\ mod\ K\neq0 (n−1) mod K=0,故结点 n n n 的右兄弟编号为 n + 1 n+1 n+1
【图遍历与二叉树遍历的联系】
DFS 相当于二叉树中的先序遍历,BFS 相当于二叉树中的层次遍历
【最小生成树】
含有 n n n 个顶点的单权连通图的最小生成树是指图中任意一个由 n n n 个顶点构成的边的权值之和最小的连通子图
【普里姆算法和克鲁斯卡尔算法】
普里姆算法的复杂度为 O ( n 2 ) \mathcal O(n^2) O(n2),只与图中的顶点数有关,所以适合稠密图
克鲁斯卡尔算法时间主要花费在对边的排序上,所以时间复杂度与边数有关,适合稀疏图
【图的两种遍历的适用范围】
DFS、BFS 对任何图都适用,并没有限制是针对有向图还是无向图
【强连通图】
有 n n n 个顶点的强连通图最多有 n ( n − 1 ) n(n-1) n(n−1) 条边(每一对顶点一条边),最少 n n n 条边(成环)
【习题】
用DFS遍历一个无环有向图,并在DFS算法退栈返回时打印相应的顶点,则输出的顶点序列是( )。
【解析】逆拓扑有序,如果是队列:拓扑有序,如果是栈:逆拓扑有序
【习题】
一个具有 n 个顶点的无向连通图,它所包含的连通分量数为( )
A. 0
B. 1
C. n
D. 不确定
【解析】B
无向图 G 的极大连通子图称为 G 的连通分量。任何连通图的连通分量只有一个,即是其自身,非连通的无向图有多个连通分量。(如果题目没有说连通图,则具有 n 个顶点的额无向图最少有 1 个连通分量,最多有 n 个连通分量)
【习题】
在一个包含n个顶点的有向图中,如果所有顶点的出度之和为s,则所有顶点的入度之和为( )
【解析】s
在有向图中,对于每一条边都有对应的入点和出点,因此入度之和与出度之和一致
【习题】
【解析】B,看清楚是有向图,有向图连通比无向图在最少边时多一条,因为要连回去
【习题】
用有向无环图描述表达式(A+B)*((A+B)/A),至少需要顶点的数目为()
【解析】5
先看个例子
先将表达式转化为二叉树,再将二叉树去重转换成有向无环图,这里的去重是指去除重复的节点,故对本题有
【习题】
深度优先搜索和拓扑排序算法都可以判断一个有向图中是否有环
【解析】正确
对于DFS,向下搜索过程中如果搜索到前面已经走过的节点,即可以说明有环
对于拓扑排序,访问不到图中所有的节点,亦可以说明存在回路
【习题】
若用邻接矩阵存储有向图,矩阵中主对角线以下的元素均为零,则关于该图拓扑序列的结论是()。
【解析】一定存在,可能不唯一
如. [ 0 1 1 0 0 1 0 0 0 ] \begin{bmatrix} 0 & 1 &1 \\ 0&0& 1\\ 0&0 &0 \end{bmatrix} ⎣⎡000100110⎦⎤和 [ 0 1 1 0 0 0 0 0 0 ] \begin{bmatrix} 0 & 1 &1 \\ 0&0& 0\\ 0&0 &0 \end{bmatrix} ⎣⎡000100100⎦⎤
【习题】
下列关于AOE网的叙述中,不正确的是()
A 关键活动不按期完成就会影响整个工程的完成时间
B 任何一个关键活动提前完成,那么整个工程将会提前完成
C 所有关键活动提前完成,那么整个工程将会提前完成
D 某些键活动提前完成,那么整个工程将会提前完成
【解析】B
关键路径延期完成,必将导致关键路径长度增加,即整个工程的最短完成时间增加。但是关键路径不止一条,当存在多条关键路径的时候,其中一条上的关键活动时间缩短,只能导致该条关键路径变成非关键路径,而不会影响整个工程的最短完成时间。对于 A)、B)来说,就是关键路径上的某个关键活动延期完成,必然导致其他的关键路径变为非关键路径。而缩短某些关键活动,则不一定缩短整个工程的完成时间
【习题】
下列关于图的叙述中,正确的是()。
Ⅰ.回路是简单路径
Ⅱ.存储稀疏图,用邻接矩阵比邻接表更省空间
Ⅲ.若有向图中存在拓扑序列,则该图不存在回路
【解析】仅Ⅲ
如果路径上的各顶点均不互相重复,称这样的路径为简单路径
【习题】
下列关于无向连通图特性的叙述中,正确的是()。
Ⅰ.所有顶点的度之和为偶数
Ⅱ.边数大于顶点个数减1
Ⅲ.至少有一个顶点的度为1
【解析】只有Ⅰ
Ⅱ.边数大于等于顶点个数减1
Ⅲ.无向完全图中不存在度为1的顶点
一个无向图 G = ( V , E ) G=(V,E) G=(V,E) 是连通的,那么边的数目大于等于顶点的数目减一: ∣ E ∣ > = ∣ V ∣ − 1 |E|>=|V|-1 ∣E∣>=∣V∣−1,而反之不成立
【习题】
对有 n n n 个结点、 e e e 条边且使用邻接表存储的有向图进行广度优先遍历,其算法时间复杂度是( )
【解析】 O ( n + e ) \mathcal O(n+e) O(n+e)
【习题】
如下有向带权图,若采用迪杰斯特拉(Dijkstra) 算法求源点a到其他各顶点的最短路径,得到的第一条最短路径的目标顶点是b,第二条最短路径的目标顶点是c,后续得到的其余各最短路径的目标顶点依次是( )
A d,e,f
B e,d,f
C f,d,e
D f,e,d
【解析】C
【习题】
下列 AOE 网表示一项包含 8个活动的工程。通过同时加快若干进度可以缩短整个工程的工期。下列选项中,加快其进度就可以缩短工程工期的是( )。
A c和e
B d和c
C f和d
D f和h
【解析】C
【习题】
【解析】其实这是最小生成树的问题,按照克鲁斯卡尔算法可以得到两个结果
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;
}
我们设从起点到 A A A 中各结点的距离均为 L L L(私认为重点不要是没啥问题的),于是根据迪杰斯特拉算法可以求出从起点到图中其余各点的最短路径,那么 a → b a\rightarrow b a→b 的最短路径就是上述序列中第二个顶点为 a a a、最后一个顶点为 b b b 的最短路,然后最短距离减去 L L L 即为 a , b a,b a,b 间的最短距离
【习题】
【解析】显然还是不正确的,如
【习题】
【解析】
【习题】
【解析】
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;
}
}
【知识点总结】
注:希尔排序的时间是所取“增量”序列的函数,当 n n n 在某个特定范围内,希尔排序所需的比较和移动次数约为 n 1.3 n^{1.3} n1.3
快排时间复杂度的分析:对长度为 L L L 的子序列进行一次划分,其基本操作可取为两指针的移动,总共 L − 1 L-1 L−1 次
不稳定的:情绪不稳定,快些选一堆好友来聊天
复杂度 O ( n l o g n ) \mathcal O(nlogn) O(nlogn): 快些 以nlogn 归队
各排序算法什么情况下最好,什么情况下最坏:
一趟排序后能选出一个关键字放在其最终位置上的算法:简单选择、堆排、快排、冒泡
排序趟数和序列的初始状态有关的算法:交换类的排序,其趟数和原始序列状态有关
直接插入排序趟数固定为n-1
简单选择排序趟数固定为n-1
基排每趟都要分配和收集,排序趟数固定为d
比较次数与序列原始状态无关的是简单选择(还有一个折半插入),因为无论序列初始状态如何,每趟排序选择最小(大)值,都要顺序遍历序列,依次用当前最小值和序列中的当前值比较
简单选择排序的比较次数为 O ( n 2 ) \mathcal O(n^2) O(n2),交换次数为 O ( n ) \mathcal O(n) O(n)
【习题】
【解析】A
注意区分一下简单选择和冒泡排序,冒泡的第一趟结果应该是 { 47 , 25 , 15 , 21 , 84 } \left\{ 47, 25,15,21,84\right\} {47,25,15,21,84}
【习题】
【解析】C、D
有序的结果为 { 11 , 18 , 23 , 68 , 69 , 73 , 93 } \left\{11,18,23,68,69,73,93\right\} {11,18,23,68,69,73,93} 或者反过来两种情况
【习题】
采用递归方式对顺序表进行快速排序,下列关于递归次数的叙述中,正确的是
A 递归次数与初始数据的排列次序无关
B 每次划分后,先处理较长的分区可以减少递归次数
C 每次划分后,先处理较短的分区可以减少递归次数
D 递归次数与每次划分后得到的分区处理顺序无关
【解析】D
【习题】
【解析】B
第一比较: 10 < 18 10<18 10<18,第二次比较: 18 < 25 18<25 18<25
但是本题答案选项有一点小问题,因为堆排序代码中需要对子树根结点的两个孩子结点做一次比较,以选出较大的,再与子树根结点比较。所以,严格的比较次数应该多出来一次 13 13 13 和 18 18 18 的比较,即通过比较挑出较大 18 18 18 再和根结点 25 25 25 比较,所以正确的关键字比较次数应该是 3 3 3
【习题】
对同一待排序列分别进行折半插入排序和直接插入排序,两者之间可能的不同之处是()
A.排序的总趟数
B.元素的移动次数
C.使用辅助空间的数量
D.元素之间的比较次数
【解析】D
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++;
}
}
【习题】
【解析】
1)堆排序 O ( 1 ) \mathcal O(1) O(1),快排 O ( l o g 2 n ) \mathcal O(log_2n) O(log2n),归并 O ( n ) \mathcal O(n) O(n)
2)只有归并排序是稳定排序
3)在平均情况下来看,在时间复杂度同为 O ( n l o g n ) \mathcal O(nlogn) O(nlogn) 的所有算法中,快排的基本操作执行次数最少,虽然数量级是一样的,但是实际中快排会更快一些
4)堆排序,因为其最坏情况下也是 O ( n l o g n ) \mathcal O(nlogn) O(nlogn),空间复杂度为 O ( 1 ) \mathcal O(1) O(1)
【习题】
【解析】
【注】 I / O 次 数 = 带 权 路 径 长 度 × 2 I/O次数 = 带权路径长度 \times 2 I/O次数=带权路径长度×2
具体内容可参见最佳归并树的知识,该题和最佳归并树一样
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)