思维导图
目录
算法思想比较
动态规划和贪心算法有什么区别
贪心和动态规划都是要求原问题拥有最优子结构。二者的区别在于贪心是在每次选择时,通过一种固定的策略去选择,没有被选择的子问题则不去考虑。例如在纸币问题中,给定具体几个面额的纸币,例如11 5 1,使得凑够15块钱所需要的纸币数量最少,贪心的策略是每次选择其中面值最大纸币,以此得到的结果是1张11,4张1,一共5张,但是实际上只需3张5即可。
而动态规划则是对所有子问题进行考虑选择继承其中能够得到最优解的那个,对暂时没有被继承的子问题,由于动态规划重叠子问题的存在,后期可能会再次考虑,因此还有可能成为全局最优解。
所以贪心可以说是一种选择了就要一直选择下去不后悔的方式。而动态规划则是从全局角度上来说看谁笑到最后,不必太过在意暂时的“领先”。
数据结构比较
链表和数组的区别
1、 存取操作 2、插入、查找、删除操作 3、逻辑结构和物理结构 4、存储
1.存取方式
顺序表可以顺序存取,也可以随机存取,链表只能从表头存取元素。
2.逻辑结构与物理结构
采用顺序存储时,逻辑上相邻的元素,其对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素,其物理存储位置则不一定相邻,其对应的逻辑关系是通过指针链接来表示的。
3.查找、插入和删除操作
对于按值查找,顺序表无序时,两者的时间复杂度均为O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为O(log2n)。
对于按序号查找,顺序表支持随机访问,时间复杂度仅为O(1),而链表的平均时间复杂度为O(n)。顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需修改相关结点的指针域即可。由于链表的每个结点都带有指针域,因而在存储空间上要比顺序存储付出的代价大,而存储密度不够大。
4.空间分配
顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。动态存储分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。
数据结构--树
二叉树两个结点的最近公共祖先
参考链接:https://blog.csdn.net/qinzhaokun/article/details/50640628
方法一:一个简单的复杂度为 O(n) 的算法,解决LCA问题
1) 找到从根到n1的路径,并存储在一个向量或数组中。
2)找到从根到n2的路径,并存储在一个向量或数组中。
3) 遍历这两条路径,直到遇到一个不同的节点,则前面的那个即为最低公共祖先.
bool findpath(Node * root,vector<int> &path,int key){
if(root == NULL) return false;
path.push_back(root->key);
if(root->key == key) return true;
//左子树或右子树 是否找到,找到的话当前节点就在路径中了
bool find = ( findpath(root->left, path, key) || findpath(root->right,path ,key) );
if(find) return true;
//该节点下未找到就弹出
path.pop_back();
return false;
}
int findLCA(Node * root,int key1,int key2){
vector<int> path1,path2;
bool find1 = findpath(root, path1, key1);
bool find2 = findpath(root, path2, key2);
if(find1 && find2){
int ans ;
for(int i=0; i<path1.size(); i++){
if(path1[i] != path2[i]){
break;
}else
ans = path1[i];
}
return ans;
}
return -1;
}
方法二:
上面的方法虽然是O(n),但是操作依然繁琐了一点,并且需要额外的空间来存储路径。其实可以只遍历一次,利用递归的巧妙之处。
从root开始遍历,如果n1和n2中的任一个和root匹配,那么root就是LCA。 如果都不匹配,则分别递归左、右子树,如果有一个 key(n1或n2)出现在左子树,并且另一个key(n1或n2)出现在右子树,则root就是LCA. 如果两个key都出现在左子树,则说明LCA在左子树中,否则在右子树。
struct Node *findLCA(struct Node* root, int n1, int n2)
{
if (root == NULL) return NULL;
// 只要n1 或 n2 的任一个匹配即可
// (注意:如果 一个节点是另一个祖先,则返回的是祖先节点。因为递归是要返回到祖先的 )
if (root->key == n1 || root->key == n2)
return root;
// 分别在左右子树查找
Node *left_lca = findLCA(root->left, n1, n2);
Node *right_lca = findLCA(root->right, n1, n2);
// 如果都返回非空指针 Non-NULL, 则说明两个节点分别出现了在两个子树中,则当前节点肯定为LCA
if (left_lca && right_lca) return root;
// 如果一个为空,在说明LCA在另一个子树
return (left_lca != NULL)? left_lca: right_lca;
}
二叉树遍历
参考链接:https://blog.csdn.net/zhangxiangdavaid/article/details/37115355
https://www.yunaitong.cn/binary-tree-traverse.html
前序遍历
①递归实现
//前序遍历
void preorder(TreeNode *root, vector<int> &path)
{
if(root != NULL)
{
path.push_back(root->val);
preorder(root->left, path);
preorder(root->right, path);
}
}
②非递归实现
入栈时访问是前序遍历!
//前序遍历
//Binary Tree Node
typedef struct node
{
int data;
struct node* lchild; //左孩子
struct node* rchild; //右孩子
}BTNode;
void PreOrderWithoutRecursion2(BTNode* root)
{
if (root == NULL)
return;
BTNode* p = root;
stack<BTNode*> s;
while (!s.empty() || p)
{
if (p)
{
cout << p->data;
//边遍历边打印,并存入栈中,以后需要借助这些根节点进入右子树
s.push(p);
p = p->lchild;
}
else
{
p = s.top();
s.pop();
p = p->rchild;
}
}
cout << endl;
}
中序遍历
①递归实现
//中序遍历
void inorder(TreeNode *root, vector<int> &path)
{
if(root != NULL)
{
inorder(root->left, path);
path.push_back(root->val);
inorder(root->right, path);
}
}
②非递归实现
利用栈来一直找到左子树最下面的结点。
出栈时访问是中序遍历!
代码:
//中序遍历
//Binary Tree Node
typedef struct node
{
int data;
struct node* lchild; //左孩子
struct node* rchild; //右孩子
}BTNode;
void InOrderWithoutRecursion2(BTNode* root)
{
//空树
if (root == NULL)
return;
//树非空
BTNode* p = root;
stack<BTNode*> s;
while (!s.empty() || p)
{
if (p)
{
s.push(p);
p = p->lchild;
}
else
{
p = s.top();
s.pop();
cout << p->data;
p = p->rchild; //继续进行循环
}
}
}
后序遍历
①递归实现
//后续遍历
void postorder(TreeNode *root, vector<int> &path)
{
if(root != NULL)
{
postorder(root->left, path);
postorder(root->right, path);
path.push_back(root->val);
}
}
②非递归实现
参考:https://www.jianshu.com/p/456af5480cee
后序遍历在决定是否可以输出当前节点的值的时候,需要考虑其左右子树是否都已经遍历完成。
所以需要设置一个lastVisit游标。
若lastVisit等于当前考查节点的右子树,表示该节点的左右子树都已经遍历完成,则可以输出当前节点。
并把lastVisit节点设置成当前节点,将当前游标节点node设置为空,下一轮就可以访问栈顶元素。
否则,需要接着考虑右子树,node = node.right。
//后序遍历
void PostOrderWithoutRecursion(BTNode* root)
{
if (root == NULL)
return;
stack<BTNode*> s;
//pCur:当前访问节点,pLastVisit:上次访问节点
BTNode* pCur, *pLastVisit;
pCur = root;
pLastVisit = NULL;
while (pCur != null || !s.empty() ) {
while (pCur != null) {
s.push(pCur);
pCur = pCur.left;
}
//查看当前栈顶元素
pCur = s.top();
//如果其右子树也为空,或者右子树已经访问
//则可以直接输出当前节点的值
if ( pCur.right == null || pCur.right == lastVisit) {
cout<<pCur.val<<" ";
s.pop();
pLastVisit = pCur;
pCur = null;
} else {
//否则,继续遍历右子树
pCur = pCur.right;
}
}
cout << endl;
}
参考一个pdf
数据结构常考题
链表队列 树 图 算法..
【牛客网上收藏夹】
栈和队列的共同特点是
栈通常采用的两种存储结构是
在单链表中,增加头结点的目的是
循环链表的主要优点是
线性表的顺序存储结构和线性表的链式存储结构分别是