欢迎来到Cefler的博客😁
🕌博客主页:那个传说中的man的主页
🏠个人专栏:题目解析
🌎推荐文章:题目大解析(3)
目录
👉🏻二叉搜索树
概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
即:左<根<右
👉🏻二叉搜索树模拟实现(1)
Insert插入
bool Insert(const K& key)
{
if (_root == nullptr)//如果为空直接创建新结点
{
_root = new Node(key);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)//key比当前cur结点的key值小往左边走,大则往右边走,直到遇到空
{
parent = cur;
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key);//此时遇到空要创建新结点
//但此时我们还要记得将其与parent连接起来,至于是在parent的左边还是右边,看比大小
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
过程即为:小于往左走,大于往右走,遇到空创建新结点,进行连接父节点
find查找
bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
return true;//相等说明找到,返回true
}
return false;
}
中序遍历
void _InOrder(Node* root)//中序遍历
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
void InOrder()//套一层
{
_InOrder(_root);
cout << endl;
}
Erase删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情
况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
而a情况可以归属于b和c任意一个情况,a情况当把结点删除后,父节点想向指向左右哪边都行,反正都为空无所谓。
- 情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除
- 情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除
- 情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点
中,再来处理该结点的删除问题–替换法删除
替换法删除
:
1.要么从左子树中找到最大结点来替换
2.要么从右子树找到最小结点来替换
所以现在梳理一下代码思路:
1.先找到要删除的结点位置,能找到再删除,找不到返回false
2.开始删除,判情况(b,c,d),对症下药
代码如下:
bool Erase(const K& key)
{
Node* cur = _root;
Node* parent = nullptr;
while (cur)//寻找要删除的结点
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
//找到了就可以开始删除了,但是要判情况,对症下药
if (cur->_left == nullptr)
{
//左边为空,则删除后,将父节点连接cur的右边
if (cur == _root)//如果要删除的结点就是初始根结点
{
_root = cur->_right;
}
//先确定此时cur在父节点的左边还是右边
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
else if (cur->_right == nullptr)
{
//右边为空
if (cur == _root)//如果要删除的结点就是初始根结点
{
_root = cur->_left;
}
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
else
{
//左右都不为空,此时用替换法
//这里我们以寻找右边树最小值的替换法执行;而右边树最小值即可以认为就是右边树最左边结点
Node* parent = cur;
Node* subleft = cur->_right;
while (subleft->_left)//当左边遇到空,说明此时subleft已经遍历到最左边
{
parent = subleft;
subleft = subleft->_left;
}
//找到后,可以进行交换了
swap(cur->_key, subleft->_key);
//现在进行删除,而且删除情况属于a情况
if (subleft == parent->_left)
{
parent->_left = subleft->_right;//这边=subleft->_right或者subleft->_left都可以
}
else
{
parent->_right = subleft->_right;
}
}
return true;
}
}
return false;
}
InsertR递归插入
bool _InsertR(Node*& root, const K& key)//递归插入
{
if (root == nullptr)
{
root = new Node(key);//传引用的好处就是此时的root就是其父节点的左/右节点,无需记录父节点
return true;
}
if (root->_key > key)
{
_InsertR(root->_left, key);
}
else if (root->_key < key)
{
_InsertR(root->_right, key);
}
else
return false;
}
FindR递归查找
bool _FindR(Node* root, const K& key)//递归查找
{
if (root == nullptr)
return false;
if (root->_key > key)
{
FindR(root->_left, key);
}
else if (root->_key < key)
{
FindR(root->_right, key);
}
else
return true;
}
EraseR递归删除
bool _EraseR(Node*& root, const K& key)//这里我们仍然用引用root,这样连接时就不用记录父节点了
{
if (root == nullptr)
return false;
if (root->_key < key)
{
return _EraseR(root->_right, key);
}
else if (root->_key > key)
{
return _EraseR(root->_left, key);
}
else
{
//开始删除
if (root->_left == nullptr)
{
//左为空
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
//左右都不为空
//这里以寻找右子树最小值
Node* subleft = root->_right;
while (subleft->_left)
{
subleft = subleft->_left;
}
swap(root->_key, subleft->_key);
// 转换成在子树去递归删除
return _EraseR(root->_right, key);
}
}
}
析构函数
~BSTree()
{
Destroy(_root);
}
void Destroy(Node*& root)
{
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
👉🏻二叉搜索树模拟实现(2)
拷贝构造函数
BTTree(const BSTree<K>& bt)
{
_root = Copy(bt._root);
}
Node* Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* newRoot = new Node(root->_key);
newRoot->_left = Copy(root->_left);
newRoot->_right = Copy(root->_right);
return newRoot;
}
赋值重载运算符
BSTree<K>& operator=(BSTree<K> bt)
{
swap(_root, bt._root);
return *this;
}
🍒BinarySearchTree.h
#pragma once
#include <iostream>
using namespace std;
template <class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
};
template <class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
//构造函数
BSTree(){}
//拷贝构造函数
BSTree(const BSTree<K>& bt)
{
_root = Copy(bt._root);
}
bool Insert(const K& key)
{
if (_root == nullptr)//如果为空直接创建新结点
{
_root = new Node(key);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)//key比当前cur结点的key值小往左边走,大则往右边走,直到遇到空
{
parent = cur;
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key);//此时遇到空要创建新结点
//但此时我们还要记得将其与parent连接起来,至于是在parent的左边还是右边,看比大小
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
return true;//相等说明找到,返回true
}
return false;
}
bool Erase(const K& key)
{
Node* cur = _root;
Node* parent = nullptr;
while (cur)//寻找要删除的结点
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
//找到了就可以开始删除了,但是要判情况,对症下药
if (cur->_left == nullptr)
{
//左边为空,则删除后,将父节点连接cur的右边
if (cur == _root)//如果要删除的结点就是初始根结点
{
_root = cur->_right;
}
//先确定此时cur在父节点的左边还是右边
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
else if (cur->_right == nullptr)
{
//右边为空
if (cur == _root)//如果要删除的结点就是初始根结点
{
_root = cur->_left;
}
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
else
{
//左右都不为空,此时用替换法
//这里我们以寻找右边树最小值的替换法执行;而右边树最小值即可以认为就是右边树最左边结点
Node* parent = cur;
Node* subleft = cur->_right;
while (subleft->_left)//当左边遇到空,说明此时subleft已经遍历到最左边
{
parent = subleft;
subleft = subleft->_left;
}
//找到后,可以进行交换了
swap(cur->_key, subleft->_key);
//现在进行删除,而且删除情况属于a情况
if (subleft == parent->_left)
{
parent->_left = subleft->_right;//这边=subleft->_right或者subleft->_left都可以
}
else
{
parent->_right = subleft->_right;
}
}
return true;
}
}
return false;
}
void InOrder()//套一层
{
_InOrder(_root);
cout << endl;
}
bool FindR(const K& key)
{
return _FindR(_root, key);
}
bool InsertR(const K& key)
{
return _InsertR(_root,key);
}
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
~BSTree()
{
Destroy(_root);
}
BSTree<K>& operator=(BSTree<K> bt)
{
swap(_root, bt._root);
return *this;
}
private:
Node* Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* newRoot = new Node(root->_key);
newRoot->_left = Copy(root->_left);
newRoot->_right = Copy(root->_right);
return newRoot;
}
void Destroy(Node*& root)
{
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
bool _EraseR(Node*& root, const K& key)//这里我们仍然用引用root,这样连接时就不用记录父节点了
{
if (root == nullptr)
return false;
if (root->_key < key)
{
return _EraseR(root->_right, key);
}
else if (root->_key > key)
{
return _EraseR(root->_left, key);
}
else
{
//开始删除
if (root->_left == nullptr)
{
//左为空
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
//左右都不为空
//这里以寻找右子树最小值
Node* subleft = root->_right;
while (subleft->_left)
{
subleft = subleft->_left;
}
swap(root->_key, subleft->_key);
// 转换成在子树去递归删除
return _EraseR(root->_right, key);//root->_right或者root->_left都可以,反正都是空
}
}
}
bool _InsertR(Node*& root, const K& key)//递归插入
{
if (root == nullptr)
{
root = new Node(key);//传引用的好处就是此时的root就是其父节点的左/右节点,无需记录父节点
return true;
}
if (root->_key > key)
{
_InsertR(root->_left, key);
}
else if (root->_key < key)
{
_InsertR(root->_right, key);
}
else
return false;
}
bool _FindR(Node* root, const K& key)//递归查找
{
if (root == nullptr)
return false;
if (root->_key > key)
{
FindR(root->_left, key);
}
else if (root->_key < key)
{
FindR(root->_right, key);
}
else
return true;
}
void _InOrder(Node* root)//中序遍历
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
Node* _root = nullptr;
};
👉🏻二叉搜索树改造key_value模型
概念
KV模型
:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方
式在现实生活中非常常见:
- 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文<word, chinese>就构成一种键值对; - 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是<word, count>就构成一种键值对
模拟实现
#pragma once
#include<iostream>
using namespace std;
namespace kv
{
template <class K,class V>
struct BSTreeNode
{
BSTreeNode<K,V>* _left;
BSTreeNode<K,V>* _right;
K _key;
V _value;
BSTreeNode(const K& key,const V& value)
:_left(nullptr)
, _right(nullptr)
, _key(key)
, _value(value)
{}
};
template <class K,class V>
class BSTree
{
typedef BSTreeNode<K,V> Node;
public:
bool Insert(const K& key,const V& value)
{
if (_root == nullptr)//如果为空直接创建新结点
{
_root = new Node(key,value);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)//key比当前cur结点的key值小往左边走,大则往右边走,直到遇到空
{
parent = cur;
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key,value);//此时遇到空要创建新结点
//但此时我们还要记得将其与parent连接起来,至于是在parent的左边还是右边,看比大小
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
return cur;
}
return nullptr;
}
bool Erase(const K& key)
{
Node* cur = _root;
Node* parent = nullptr;
while (cur)//寻找要删除的结点
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
//找到了就可以开始删除了,但是要判情况,对症下药
if (cur->_left == nullptr)
{
//左边为空,则删除后,将父节点连接cur的右边
if (cur == _root)//如果要删除的结点就是初始根结点
{
_root = cur->_right;
}
//先确定此时cur在父节点的左边还是右边
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
else if (cur->_right == nullptr)
{
//右边为空
if (cur == _root)//如果要删除的结点就是初始根结点
{
_root = cur->_left;
}
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
else
{
//左右都不为空,此时用替换法
//这里我们以寻找右边树最小值的替换法执行;而右边树最小值即可以认为就是右边树最左边结点
Node* parent = cur;
Node* subleft = cur->_right;
while (subleft->_left)//当左边遇到空,说明此时subleft已经遍历到最左边
{
parent = subleft;
subleft = subleft->_left;
}
//找到后,可以进行交换了
swap(cur->_key, subleft->_key);
//现在进行删除,而且删除情况属于a情况
if (subleft == parent->_left)
{
parent->_left = subleft->_right;//这边=subleft->_right或者subleft->_left都可以
}
else
{
parent->_right = subleft->_right;
}
}
return true;
}
}
return false;
}
void InOrder()//套一层
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)//中序遍历
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
Node* _root = nullptr;
};
}
key_valueTest.cpp
#include "key_value.h"
int main()
{
kv::BSTree<string, string> dict;
dict.Insert("love", "爱");
dict.Insert("is", "是");
dict.Insert("stream", "细水长流");
kv::BSTreeNode<string, string>* it;
string str;
while (cin >> str)
{
it = dict.Find(str);
if (it)
{
cout << it->_value << endl;
}
else
cout << "None" << endl;
}
return 0;
}
✍🏻二叉树题目
根据二叉树创建字符串
原题链接:根据二叉树创建字符串
1.左右为空可以省略
2.右为空,左不为空可以省
3.左为空,右不为空,不能省,省了就不能确定左右哪个空了
mycode:
class Solution {
public:
string tree2str(TreeNode* root) {
string str;
if(root==nullptr)
return str;
str += to_string(root->val);
//先处理左子树
if(root->left||root->right)//左不为空||左为空,右不为空,此时左边不能省略括号
{
str+='(';
str += tree2str(root->left);
str+= ')';
}
//再处理右子树
if(root->right)//右不为空
{
str+='(';
str += tree2str(root->right);
str+= ')';
}
return str;
}
};
二叉树的最近公共祖先
原题链接:二叉树的最近公共祖先
法一:左右法
mycode:
class Solution {
public:
bool isHere(TreeNode* root,TreeNode* x)
{
if(root==nullptr)
return false;
if(root==x)
return true;
return isHere(root->left,x)||isHere(root->right,x);
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root==p||root==q)
return root;
bool pInLeft = isHere(root->left,p);
bool pInRight = !pInLeft;//不是在左就是在右
bool qInLeft = isHere(root->left,q);
bool qInRight = !qInLeft;
if((pInLeft&&qInRight)
||(qInLeft&&pInRight))//如果p、q在结点的两侧
{
return root;
}
else if(pInLeft&&qInLeft)//都在左
{
return lowestCommonAncestor(root->left,p,q);
}
else if(pInRight&&qInRight)//都在右
{
return lowestCommonAncestor(root->right,p,q);
}
return nullptr;//随便给个值敷衍一下编译器
}
};
法二:链表相交法
class Solution {
public:
bool FindPath(TreeNode* root,TreeNode* x,stack<TreeNode*>& st)
{
if(root==nullptr)
return false;
st.push(root);
if(root==x)//当前结点找到
return true;
if(FindPath(root->left,x,st))//左子树找到
return true;
if(FindPath(root->right,x,st))//右子树找到
return true;
//当前结点左右都没找到
st.pop();
return false;//告诉上一结点,我这里没找到
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
stack<TreeNode*> pPath,qPath;
FindPath(root,p,pPath);
FindPath(root,q,qPath);
while(pPath.size()!=qPath.size())
{
if(pPath.size()>qPath.size())
pPath.pop();
else
qPath.pop();
}
while(pPath.top()!=qPath.top())
{
pPath.pop();
qPath.pop();
}
return pPath.top();
}
};
二叉搜索树与双向链表
原题链接:二叉搜索树与双向链表
mycode:
class Solution {
public:
void InorderConvert(TreeNode* cur,TreeNode*& prev)
{
if(cur==nullptr)
return ;
InorderConvert(cur->left,prev);//prev给引用就是因为这里递归会改变prev,要让下面的prev同步改变,就必须要使用引用
cur->left = prev;
if(prev)//prev可能为空
prev->right = cur;
prev = cur;
InorderConvert(cur->right,prev);
}
TreeNode* Convert(TreeNode* pRootOfTree) {
TreeNode* root = pRootOfTree,*prev = nullptr;
InorderConvert(root,prev);
//接下来要找头节点,也就是原二叉搜索树的最左结点
TreeNode* head = prev;
while(head&&head->left)//前者防止本身案例为空
{
head = head->left;
}
return head;
}
};
从前序与中序遍历序列构造二叉树
- 前序先确定根节点
- 中序根据根节点划分左右子树区间
原题链接:从前序与中序遍历序列构造二叉树
mycode:
class Solution {
public:
TreeNode* _build(vector<int>& preorder, vector<int>& inorder,int& previ,int ibegin,int iend)
{
if(ibegin>iend)
return nullptr;
//先从中序数组找到根
int cur = ibegin;
while(preorder[previ]!=inorder[cur])
{
cur++;
}
//先序确定根
TreeNode* root = new TreeNode(preorder[previ++]);
//中序确定左右子树区间;[ibegin,cur-1],[cur+1,iend]
root->left = _build(preorder,inorder,previ,ibegin,cur-1);
root->right = _build(preorder,inorder,previ,cur+1,iend);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
int previ = 0;
TreeNode* root = _build(preorder,inorder,previ,0,inorder.size()-1);
return root;
}
};
从中序与后序遍历序列构造二叉树
- 后序确定根节点
- 中序划分左右子树区间
原题链接:从中序与后序遍历序列构造二叉树
mycode:
class Solution {
public:
TreeNode* _build(vector<int>& inorder, vector<int>& postorder,int& endi,int ibegin,int iend)
{
if(ibegin>iend)
return nullptr;
//先从中序数组找到根
int cur = ibegin;
while(postorder[endi]!=inorder[cur])
{
cur++;
}
//后序确定根
TreeNode* root = new TreeNode(postorder[endi--]);
//中序确定左右子树区间;[ibegin,cur-1],[cur+1,iend]
//先right再left是因为endi在右边开始起步
root->right = _build(inorder,postorder,endi,cur+1,iend);
root->left = _build(inorder,postorder,endi,ibegin,cur-1);
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
int endi = postorder.size()-1;
TreeNode* root = _build(inorder,postorder,endi,0,inorder.size()-1);
return root;
}
};
二叉树的前序遍历非递归
原题链接:二叉树的前序遍历
- 左路节点
- 左路节点的右子树
mycode:
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> v;
TreeNode* cur = root;
while(cur||!st.empty())//当cur不为空或者st为空时
{
//先存入左节点
while(cur)
{
st.push(cur);
v.push_back(cur->val);
cur = cur->left;
}
//再处理子问题——左节点的右子树
cur = st.top()->right;
st.pop();
}
return v;
}
};
二叉树的中序遍历非递归
原题链接:二叉树的中序遍历非递归
- 左路节点
- 根节点和右子树
mycode:
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> v;
TreeNode* cur = root;
while(cur||!st.empty())
{
//先存入左节点
while(cur)
{
st.push(cur);
cur = cur->left;
}
//再处理根和右子树
v.push_back(st.top()->val);//直接插入左
cur = st.top()->right;
st.pop();
}
return v;
}
};
二叉树的后序遍历非递归
原题链接:二叉树的后序遍历非递归
version1:超出时间限制
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> v;
TreeNode* cur = root;
while(cur||!st.empty())
{
while(cur)
{
st.push(cur);
cur = cur->left;
}
if(!st.top()->left&&!st.top()->right)
{
TreeNode* p = st.top();
v.push_back(st.top()->val);
st.pop();
if(st.top()->left==p)
st.top()->left = nullptr;
if(st.top()->right==p)
st.top()->right == nullptr;
}
cur = st.top()->right;
}
return v;
}
};
version2
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> v;
TreeNode* cur = root;
TreeNode* prev = prev;
while(cur||!st.empty())
{
while(cur)
{
st.push(cur);
cur = cur->left;
}
TreeNode* top = st.top();
if(top->right==nullptr||top->right == prev)
{
st.pop();
v.push_back(top->val);
prev = top;
}
else
{
cur = top->right;
}
}
return v;
}
};
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长