本博客用于初学数据结构的练习题目记录
文章目录
链表
21:合并两个有序链表
(1)学习方法: 递归解决(空间开销最小)
1:递归
解题思路: 合并两个有序链表,即将两个链表按照大小有序排列。当其中一个链表为空表时,返回另一个链表。
要想连接链表节点,首先进行大小比较,如果l1->val < l2->val 还要考虑l1->next与l2的大小关系,此时明显有着递归关系。 同样如果l1->val > l2->val 则将l2->next 与 l1继续比较 。 通过比较确定next节点是谁。
Node* merge(Node * l1 , Node *l2)
{
if(l1 == NULL) return l2;
if(l2 == NULL) return l1;
if(l1->val < l2->val) l1->next = merge(l1->next,l2);
//需要明确l1->next指的是l2还是原有Node
else l2->next =merge(l1,l2->next);
return (l1->val < l2->val)? l1:l2 ;
//在递归过程中,目的是得到当前节点的next指针指向那个节点
//该节点应该是min[a,b]
}
2:迭代
迭代法: 通过建立头节点是两个节点操作复制到新的链表上。
迭代操作 = 迭代器(head)+位置显示器(location),一般截至条件是l==NULL
如果操作可以用遍历实现,就可以尝试迭代法,链表的迭代注意头节点的使用
Node * merge(Node *l1 , Node *l2)
{
Node *head = new Node(k); //迭代器
Node *location = head ; //位置标识
while((l1 != NULL)&&(l2 != NULL))
{
if(l1->val <l2->val)
{
head->next = l1;
l1 = l1-next ;
}
else
{
head->next = l2 ;
l2 = l2->next;
}
head = head->next ; //向前迭代
}
if(l1 ==NULL) head->next = l2;
else if(l2 ==NULL) head->next =l1;
return (location->next);
}
2.3面试: 删除中间节点(节点覆盖)
常见的两种方法: 迭代法 or 覆盖法(仅仅给出链表中指定删除节点)
(1)做题心得
1:节点不能够直接被覆盖, node = node->next 并不是将node->节点覆盖node节点。而是使node节点编程node->next节点,链表中node节点并没有被删除.
2:节点删除的常规方法是迭代,然后使left->next = temp->next即可实现节点删除,但如果不使用迭代法来覆盖节点,此时仅仅需要使node的节点的data编程node->next节点(可以理解为伪装)然后使node->next = node->next->next节点,其中node节点没有被删除,只是node节点穿上了node->next节点的衣
void deletenode(node * p)
{
p ->val = p->next->val ;
p->next = p->next->next;
}
面试02.02:返回倒数第k个节点
(1)做题心得
个数and距离的关系
(1)倒数第k个节点与最后一个节点的距离:k-1。同理正数第k个节点与第一个节点的距离:k-1
(2)推广得出: 第k个节点与第m个节点的距离d=| k-m |
(3)倒数第k个节点与第一个节点的距离:
(倒数第k个节点是正数第n-k+1个节点)d=n-k
思路:设置fast and low两个节点,通过两个节点的同时迭代与位置关系,实现仅仅通过一次循环就可以实现问题解决。–>此处的出一种思路:以后若问题要求首先实现一次循环取得节点个数在使用一次循环得到具体节点可以转化为low fast节点,根据两者的距离关系实现一次循环迭代。
(2)方法一:两次迭代法
int kthtolast(Node *head , int k)
{
if(head->next ==NULL) return head->val;
Node *p =head;
int num=1;
while(head->next != NULL)
{
p =p->next;
num++;
}
p =head;
for(int i=0;i<n-k;i++)
p = p->next;
return p->val;
}
(3)方法二:双指针单循环法:
原理:将位标从头节点编程末尾的节点.
int kthtolast(Node *head , int k)
{
if(head->next ==NULL) return head->val;
Node *low =head;
Node *fast =head;
k--; //距离
while(k--)
{
fast = fast->next ; //移动距离
}
while(fast->next != NULL)
{
fast = fast->next;
low = low->next;
}
return low->val;
}
1290 二进制链表转整数
(1)本题尝试了两种解法: 暴力原理解法 and 位运算解法
(2)位运算知识:
<<: 想向左移动特定位数位置
10 << 1 -->100=4 0 << 1 -->0
#原理分析: 理解为进位操作,位置元素幂次增长1(读取正常排列的二进制数)
>>: 向右移动特定位数位置
10 >> 1 -->1=1 0 >> 1 -->0
#原理分析:理解为退位操作,位置元素幂次减少
(3)两种解法
①位运算法:
int getDecimalValue(Node *head)
{
int n=0;
while(head!=NULL)
{
n = (n << 1) + head->val;
//注意 要考虑优先级顺序 位运算低于算数运算 应加上括号
head = head->next;
}
return n;
}
②暴力原理求解法
int getDecimalValue(Node *head)
{
if(head->next==NULL) return head->val;
//遍历-->按位运算
Node *p =head;
int num=1 ,n=0;
while(p->next!=NULL)
{
num++;
p = p->next;
}
while(head!=NULL)
{
n += head->val * pow(2,--num); // 2^num-1
head = hed->next;
}
return n;
}
面试题24 反转链表
(1)尝试方法: 双节点法
(2)链表类题目常见方法小节
方法①单指针迭代法: 基础方法,用于链表的遍历
方法②双指针法 :
应用场景:问题常涉及到链表遍历+前后两个节点的操作
方法③位置保留法:
应用场景:如本题 由于某种操作后会导致某个指向改变,故需要保存某个节点next or head节点位置时引入心得节点变量。
(3)解法分析
①双指针法: 分析链表结构,如果能够实现两个前后指针,在每次循环中后一个指针的next指向前一个指针,在向前推进,就可以遍历实现链表反转。
ListNode* reverseList(Node* head)
{
Node *left =NULL;
Node *right =head ;
while(right!=NULL)
{
//由于next指针指向left节点,故需要新变量保存原有next节点位置
Node *location = head->next ;
right->next =left ;
left =right ;
right =location;
}
return left ;
}
②递归法 (递归实现迭代查找)
算法分析: 双指针的方法是典型的正向迭代查找,那有没有直接进行反向查找的能力呢? 囷难的地方在哪呢?
如果反向查找 1 需要有返回末位节点的能力 2 能够明确front指针,即能够操作后一个指针指向前一个指针(1:字面表示 2:前一个指针的next指针指向自己)
1:能够返回末位节点 -->return tail就可
2:能够明确前一个指针 -->若后指针直接指向前一指针(×)
or -->若前一指针使next指针指向自己,并从后往前推–>递归(操作后一个指针,返回上层又是前一个指针)
Node* reverseList(Node* head)
{
//tail-->前推并反向指
if(head==NULL||head->next==NULL)
return head;
Node* tail = reverseList(head->next);
//递归找到末尾节点,并逐层前推 realhead=head newhead=head->next
head->next->next=head;
head->next=NULL;
return tail; //反向传递得到tail
}
876链表的中间节点
(1)学习方法: 双指针迭代法
一般涉及到链表位置、距离等等,就可以用快慢指针法,利用构建两个指针的距离与操作实现一次迭代实现功能
low fast =head
low: 1 --> n/2+1 (n/2) fast : n+1 (n) 偶数
low: 1 -->n+1/2 (n-1/2) fast: n (n-1) 奇数
要得到中间节点,fast指针常用于位置标记(next==NULL or ==NULL)当fast到达末位时,low也应该达到middle(n/2+1)/(n+1/2)两者移动的关系是
n/2 or n-1/2 ---- n-1 or n 由此可以指定fast位置
Node* getmiddle(Node *head)
{
Node *fast = head;
Node *low = head;
while(fast!=NULL && fast->next!=NULL)
{
fast = fast->next->next;
low = low->next;
}
return low;
}
面试题02.07 链表相交
(1)题目分析
返回两个链表中相同的节点引用: 指的是节点的地址相同(不要单纯地理解为节点的数据与指针相同)
看完理解题目后,想到这道题就是在考察两个链表相同的节点,可以理解为前面的节点不同,共用交点过后的子链表。所以关键就是找到两个链表中相同的某个节点。
首先想到的是O(n^2)的两次while()循环迭代法,但由于时间复杂度过高不应该放在首位
(2)学习方法 :两圈相交法(实现等位查找)
两圈相交法:不实行双重迭代,两圈相交法就可以解决相交问题。它的核心就是将两端长度不同的线段合并成长度相同的线段,这样就可以实现等位查找。如果在线段中找到相同节点返回即可。如果没找到两者最终都会同时到达NULL
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
ListNode *pa =headA;
ListNode *pb = headB;
while(pa!=pb)
{
if(pa==NULL) pa = headB; //连接
else pa = pa->next;
if(pb ==NULL) pb = headA ;
else pb = pb->next;
}
return pa; //NULL or Node
//46ms
}
面试题02.01 移除重复节点 (ing hashset)
(1)学习方法: 哈希map 嵌套循环法
①嵌套循环法 O(n^2)
算法思路: 从头到尾遍历一遍,在第一层循环中每次进行后面节点的迭代判断,通过双指针实现重复节点的删除
ListNode* removeDuplicateNodes(ListNode* head)
{
ListNode *p = head;
while(p!=NULL)
{
ListNode *left = p;
ListNode *right = p->next;
while(right != NULL)
{
if(right->val == p->val)
{
left->next = right->next;
right = right->next;
}
else
{
left = left->next;
right = right->next;
}
}
p = p->next;
}
return head;
}
树
面试题04.02 最小高度树
题目: 给定一个有序数组,要求一个高度最小的二叉查找树。
(1)解题思路: 若要生成高度最小的二叉查找树,也就是此二叉树要保持平衡且最好由中位数作为节点
(2)学习方法
①有序数组中位数的求解
注意:边界的选取,由于是数组(向量)[0,size()-1] 利用此法递归二分查找各段的中位数
mid = (L+R)>>1;
//(L+R)>>1 左右边界除2取整
MinTree(L,mid-1);
MinTree(mid+1,R);
(3)本题解法
分析题目, (1)如果向量为空,则返回一个空树 (2)非空向量,逐段查找他的中位数,由于每次求解中位数的方法一致,可以采用递归法求解
TreeNode* sortedArrayToBST(vector<int>& nums)
{
if(nums.size()==0) return NULL;
return MinTree(nums,0,nums.size()-1);
};
TreeNode* MinTree(vector<int>&nums,int L,int R)
{
//二分法截至条件
if(L>R) return nullptr;
int mid = (L+R)>>1;
TreeNode *ptr = new TreeNode(nums[mid]);
ptr->left = MinTree(nums,L,mid-1);
ptr->right = MinTree(nums,mid+1,R);
return ptr;
};
剑指offer 55-1 二叉树的深度
题目:求解给定树的深度
(1)解题思路: 求解二叉树的深度问题,可以用DFS 或者 BFS求解,在这里我们先使用DFS求解。
(2)学习方法:
①DFS求解二叉树深度: 二叉树的深度可以理解为上一层节点的高度+左右子树中最大的深度。此时问题又转化为了求左右子树的深度。所以可以使用递归求解
(3)DFS解法:
int maxDepth(TreeNode* root)
{
if(root==NULL) return 0;
return max(maxDepth(root->left),maxDepth(root->right))+1;
};
剑指Offer 27 二叉树的镜像
题目:输入一个二叉树,得到二叉树的镜像
4 4
2 7 --> 7 2
1 3 6 9 9 6 3 1
(1)解题思路:问题实质上是求。二叉树子树的每层对称互换。 由于每层都要进行相同的互换,可以考虑递归算法。 输入的参数为每个子树的根节点(即是上层节点的左右指针)。 当当前根节点为空不能交换或者左右节点为空不需要交换时,既可以考虑退出递归。
(2)学习方法:
①swap()函数: 也可以进行指针,节点的互换。
(3)递归交换法
TreeNode* mirrorTree(TreeNode* root)
{
if((root ==NULL) || ((root->left == nullptr)&&(root->right == nullptr))
return root;
root->left = mirrorTree(root->left);
root->right = mirrorTree(root->right);
return root;
}
938 二叉搜索树的范围和
题目: 给定二叉树,求再给定求解范围中的所有节点数据和
(1)解题思路: 整棵树最终的到的范围和,可以理解为当前节点与左右子树在符合条件下的和。 如果当前节点不满足,则值位左右子树的和。若满足,则还要加上当前节点的数值。 由于每层都需要如此判断,故递归求解。 当当前节点为空时(本身没有数据,也没有左右子树)则return 0
(2)“子树递归求和"方法
int rangeSumBST(TreeNode* root, int L, int R)
{
if(root ==NULL) return 0;
if((root->val>=L)&&(root->val<=R))
return root->val+rangeSumBST(root->left,L,R)+rangeSumBST(root->right,L,R);
else return rangeSumBST(root->right,L,R)+rangeSumBST(root->left,L,R);
}
617 合并二叉树
题目: 给定两个二叉树,合并的节点为原节点之和。如果均为空节点则返回NULL,一个空节点则返回另一个节点值。
(1)解题思路: 要得到合并的二叉树,需要经历判断与合并两部分操作,可以在原二叉树也可以在新创建的二叉树上进行。 对于一个节点,需要判断原有节点数据存在情况,然后分情况分配新值。其左右子树节点也用相同的方法,所以运用减而治之的递归法。当原有当前位置节点均为空或本身就为空时,返回root
(2)递归合并法
TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2)
{
if((t1==NULL)&&(t2==NULL)) return nullptr;
if((t1==NULL)&&(t2!=NULL)) return t2;
if((t1!=NULL)&&(t2==NULL)) return t1;
TreeNode* current = new TreeNode(t1->val+t2->val);
current->left = mergeTree(t1->left,t2->left);
current->right = mergeTree(t1->right,t2->right);
return current;
}
700 二叉树的搜索
题目: 返回给定值再二叉树中的节点,若不存在则返沪NULL
(1)解题思路: 二叉树的搜索时二叉树ADT中最基本的操作之一,由于二叉树具备数组与链表共同的优势,所以搜索效率得到大大提高。其中重要的是搜索时左右子树的变化选取。 若采用迭代算法,截至条件为达到末位或者找到相干元素。 若采用递归算法,将问题减而治疗之。比较完当前节点,再判断比较左右子树节点,并且操作相同。递归基为找到了或者没找到返回当前节点。
(2)“递归搜索”&“迭代搜索”
//递归搜索
TreeNode* searchBST(TreeNode* root, int val)
{
if(root ==NULL) return NULL;
if(root->val ==val) return root;
return (root->val >val)? searchBST(root->left,val):searchBST(root->right,val);
}
//迭代搜索
TreeNode* searchBST(TreeNode* root, int val)
{
while(root && root->val !=val)
{
root = (root->val>val)? root->left:root->right;
}
return root; //NULL or valnode
}
590 N叉树的后序遍历
题目:给定一个N叉树,返回节点值的后续遍历
1
3| 2| 4
5 6 --> [5,6,3,2,4,1]
(1)解题思路: 后序遍历可以理解为节点值从下向上排列。 一般解决方法有迭代法和递归法,此处我们先使用递归法。 分析题目,如果要得到排序,可以先得到根节点子树的遍历序列,再将根节点的数值插入。要的到子树的遍历序列,则有需要对所有子树逐个进行相同函数运算。直到节点为空,当节点为空时不进行任何操作返回原向量即可。
(2)“递归法"
vector<int> res;
vector<int> postorder(Node* root)
{
if(root ==NULL) return res;
for(auto child:root->children)
postorder(child);
res.push_back(root->val);
return res;
}
589 N叉树的前序遍历
题目:给定一个N叉树,返回节点值的前序遍历
1
3| 2| 4
5 6 -->[1,3,5,6,2,4]
(1)解题思路: 与求解后序遍历类似,有模板迭代法和递归法。其中前序遍历可以理解为从上到下从左到右一次排序。所以相当于后序遍历就是把当前节点插入提前。 分析题目: 将问题拆分为两个问题,将当前节点值插入,将子树个节点按照当前方法插入。
(2)“递归法"
vector<int> res;
vector<int> preorder(Node* root)
{
if(root ==NULL) return res;
res.push_back(root->val);
for(auto child:root->children)
preorder(child);
return res
}
二叉树的中序、前序、后序遍历
题目:给定二叉树,返回节点值的中、前、后序遍历
(1)解题思路: 二叉树的中前后序遍历时二叉树的基础,也可以基于N叉树的遍历进行运用。算法的运行顺序不变只是子节点发生变化。
中序遍历:得到的向量按照升序排列。
后序遍历:向量按照从下到上从左到右排列
前序遍历:向量按照从左到右从上到下排列
(2)“迭代算法"
vector<int> res;
//中序遍历
void midorder(TreeNode* root)
{
if(!root) return ;
midorder(root->left);
res.push_back(root->val);
midorder(root->right);
}
//后序遍历
void postorder(TreeNode* root)
{
if(!root) return ;
postorder(root->left);
postorder(root->right);
res.push_back(root->val);
}
//前序遍历
void preorder(TreeNode* root)
{
if(!root) return;
res.push_back(root->val);
preorder(root->left);
preorder(root->right);
}
剑指Offer 54 二叉搜索树的第K大数
题目:给定二叉树与k,返回二叉树中第k大的树
(1)解题思路: 要得到第k大的数,首先需要对树进行遍历排序。由于中序排序可以直接得到树的升序排序向量,故对数进行中序排序,最后提取得到res[size()-k]
(2)“中序排序的应用"
vector<int> res;
void midorder(TreeNode* root)
{
if(!root) return;
midorder(root->left);
res.push_back(root->val);
midorder(root->right);
}
int kthLargest(TreeNode* root, int k)
{
midorder(root);
return res[res.size()-k];
}