前序:感谢学长的资料推荐以及某位算法大佬的笔记。学了数据结构和算法之后,加上一门好的编程语言刷题会达到事倍功半的效果,例如c++。
总体上分为两大部分:算法思路和代码实现。有了算法思路理解代码实现就不困难,代码实现考察coder的基本功啦。
数据结构的存储方式
数组(顺序存储)和链表(链式存储)是所有数据结构的基础,图,二叉搜索树,哈希表,B树,DFS,BFS等等高级数据结构都是其基础上延申。
两者的优缺点:
数组:空间利用率较高,效率相对较低
链表:空间利用率较低,效率相对较高
数据结构的基本操作
遍历+访问(增删改查)
形式:线性(迭代(for/while))和非线性(递归);
数组框架:迭代
void traverseArr(char* arr)
{
for(int i=0;i<arr.length();i++)
{
//访问arr[i]
}
}
二叉树框架:递归
struct BinaryTree
{
int val;
struct BinaryTree* left;
struct BinaryTree* right;
}
void traverse(struct BinaryTree *root)
{
if(root==NULL return;
traverse(root->left);
traverse(root->right);
}
链表框架:迭代加递归
struct Node
{
int val;
struct Node *next;
}node;
void traverseList(node *head)
{
for(node *p=head;p!=NULL;p=p->next)
{
//访问head->val
}
}
void traverseList(node *head)
{
if(head!=NULL)
{
//访问head->val
}
traverseList(head->next);
}
双指针技巧
目的:解决链表和数组问题
分类:
快慢指针:一快一慢,同向而行
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int fast=0,slow=0;
//int length=lengthArr(nums);
if(nums.size()==0) return 0;
while(fast<nums.size())
{
if(nums[slow]!=nums[fast])
{
slow++;
nums[slow]=nums[fast];
}
}
return slow+1;
}
};
左右指针:相向或相背而行
框架://和二叉搜索树类似
int BinarySearch(vector<int>&temp,int target)
{
int left=0,right=temp.size()-1;
while(left<=right)
{
int mid=(left+right)/2;
if(temp[mid]==target) return target;
else if(temp[mid]<target) left=mid+1;
else if(temp[mid]>target) right=mid-1;
}
}
注:数组中的下表(索引)和链表的指针作用相同
链表操作
链表结点:
struct ListNode
{
int val;
struct ListNode *next;
};
递归操作框架:反转链表
struct ListNode *traverse(struct ListNode *head)
{
if(head==NULL || head->next==NULL) return head;
struct ListNode *last = traverse(head->next);
head->next->next=head;
head->next=NULL;
return last;
}
链表进阶操作:反转链表2(反转前val个结点)
struct ListNode *traverse(struct ListNode *head,int val)
{
if(val==1)
{
//用node存储第val+1个结点
struct ListNode *node=head->next;
return head;
}
struct ListNode *last=travrse(head->next,val--);
head->next->next=head;
head->next=NULL;
return last;
}
栈和队列篇
队列主要用在BFS,栈主要用在括号判断
一种类型的括号,例如:()
bool JudgeBracket(char s)
{
int left=0;
for(int i=0;i<s.size();i++)
{
if(s[i]=='(') left++;
else left--;
if(left==-1) return false;
}
if(left==0) return true;
}
多种类型的括号,例如:()[];
char trasition(char c)
{
if(c=='}') return '{';
else if(c==']') return '[';
return '(';
}
bool verdictBracket(string s)
{
stack<char> temp;
for(char c:s)
{
if(c=='{' || c=='[' || c=='(')
{
temp.push(c);
}
else if(!temp.empty() && trasition(c)==temp.top())
{
temp.pop();
}
}
return temp.empty()==1;
}
单调栈(容易和哈希表相互结合)
核心:根据情景找到入栈出栈的方向(从前到后,从后到前)
示例:给定一个数组nums[3,4,5,2,1],请你返回一个等长的结果数组,结果数组中对应索引存储下一个更大的元素,如果没有就返回-1。
如图所示,可以把数字当作楼房。第一个后面比3大的数是4,第一个比4大的数是5。5,2,1后面没有比自身更大的数,都为-1。所以结果数组为[4,5,-1,-1,-1]。具体代码实现:
vector<int>ret(nums.size());//创建一个结果数组
stack<int>s;//创建一个栈
for(int i=nums.size();i>=0;i--)//采用倒着入栈,实际正则出栈
{
while(!s.empty() && s.top()<=nums[i])
{
s.pop();//出栈
}
ret[i]=s.empty()?-1:s.top();//s.top()中存储着当前数组元素的后面第一个比它大的值
s.push(nums[i]);
}
return ret;
单调队列+滑动窗口
1.定义:常与滑动窗口结合在一起,维护队列元素先进先出的时间顺序,同时维护滑动窗口的最值
2.应用场景:已知一个数组ret,其最值为A,给ret不断添加新值B,比较A和B得到新的最值;相反,减少一个数不适用于此方法。因为删除的数恰好是最值A,就需要重新遍历寻找最值。
例如:
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
解题核心:利用单调队列找到最值,队头元素就是最值,从队尾进行元素的更新(更新滑动窗口).采用了vector和deque容器。
函数表达:vector<int> ret,deque<int> q;
step1:先对第一个窗口查找最值
for(int i=0;i<k;i++)
{
while(!q.empty() && nums[i]>=nums[q.back()])
{
q.pop_back();
}
q.push_back(i);
}
//此时队列q的对头元素为最大值的下标
ret.push_back(q.front());
step2:进行窗口的滑动
for(int i=k;i<nums.size();i++)
{
while(!q.empty() && nums[i]>=nums[q.back()])
{
q.pop_back();
}
q.push_back(i);
//让最值nums[q.front()]在区域[q.front()-(k-1),q.front()+(k+1)]内有效
while(q.front()<=i-k)
{
q.pop_front();//更新最值
}
ret.push_back(nums[q.front()])
}
二分搜索篇
细节:对搜索区间进行精准把控
基本框架:
int BinarySearch(int *a,int target)
{
int left=0,right=...;
while(...)
{
int mid=left+(right-left)/2;
if(a[mid]==target) return target;
else if(a[mid]<target) left=...;
else if(a[mid]>target) right=...;
}
return ...;
}
mid用left+(right-left)/2而不用(left+right)/2是为了防止整型溢出
查找一个数
int BinarySearch(vector<int>&a,int target)
{
int left=0,right=a.size()-1;
while(left<=right)
{
int mid=left+(right-left)/2;
if(a[mid]==target) return target;
else if(a[mid]<target) left=mid+1;
else if(a[mid]>target) right=mid-1;
}
//退出循环,说明没找到
return -1;
}
说明:
1.left<=right与left<right的区别
搜索区间不同:前者是[left,right],后者是[left,right)
终止条件不同:前者是left=right+1,后者是left=right(终止循环时还要加上判断)
判断:return a[left]==target?target:-1
寻找左侧边界
左开右闭法
int Border_left(vector<int>&a,int target)
{
int left=0,right=a.size();
while(left<right)
{
int mid=left+(right-left)/2;
if(a[mid]==target) mid=right;
else if(a[mid]>target) right=mid;
else if(a[mid]<target) left=mid+1;
}
return left;//return right也行
}
说明:
这里返回的left代表的是比target小的数有多少个
1.为啥是right=mid和left=mid+1
本质上还是搜索区间,a[mid]判定完之后对区间[left,mid)或[mid+1,left)进行判断
2.为啥a[mid]=target时却执行mid=right
目的是寻找左侧边界,让left接近mid并且保持a[mid]=target
两边封闭法
int Left_Border(vector<int>&a,int target)
{
int left=0,right=a.size()-1;
while(left<=right)
{
int mid=left+(right-left)/2;
if(a[mid]==target) right=mid-1;
else if(a[mid]>target) right=mid-1;
else if(a[mid]<target) left=mid+1;
}
if(a[left]!=target || left>=a.size()) return -1;
return left; //也可以是right-1
}
寻找右侧边界
实质:比target小的元素个数加上同为target的元素个数
左闭右开法
int Border_right(vector<int>&a,int target)
{
int left=0,right=a.size();
while(left<right)
{
mid=left+(left-right)/2;
if(a[mid]==target) left=mid+1;
else if(a[mid]>target) right=mid;
else if(a[mid]<target) left=mid+1;
}
if(left==0) return -1;
return a[left-1]==target?left-1:-1;
}
说明:
1.if(a[mid]==target) left=mid+1是为了将左区间扩大,接近右边界
2.为啥返回值是left-1?
可由图知,left=right时结束循环,此时left指向右边界的下一个位置,所以返回left-1,right-1也行
两边封闭法
int Right_Border(vector<int>&a,int target)
{
int left=0,right=a.size()-1;
while(left<=right)
{
int mid=left+(right-left)/2;
if(a[mid]==target) left=mid+1;
else if(a[mid]<target) left=mid+1;
else if(a[mid]>target) right=mid-1;
}
if(right<0 || a[right-1]!=target) return -1;
return right;//也可以是left-1
}
说明:
right<0说明target比vector容器中的意义元素都小
二叉树篇(可以将每个二叉树结点当作root)^_^
本篇围绕递归思想展开,二叉树是一系列高级算法的基础,eg:BFS算法,DFS算法,回溯算法,动态规划,分治算法,图论算法。
递归:递进和回归,将一个问题不断分解成子问题,这是递进;将子问题又重新合并成原问题的解,这是回归,本质上就是二叉树这种数据结构。
遍历:通过一次遍历配合外部变量travese得到原问题的答案
分解:定义一个函数,通过子问题(子树)得到原问题的答案,充分利用函数的返回值
单个结点:清楚知道每个结点在什么位置(前/中/后)序做了什么事
前中后序位置:每个结点在二叉树中的位置
前中后序遍历:处理每个结点的特殊时间点
前序遍历:前序位置的代码在刚进入一个(二叉树)结点时执行
遇到⼀道⼆叉树的题目时的通用思考过程是:
1、是否可以通过遍历⼀遍⼆叉树得到答案?如果可以,⽤⼀个 traverse 函数配合外部变量来实现。
2、是否可以定义⼀个递归函数,通过⼦问题(⼦树)的答案推导出原问题的答案?如果可以,写出这个递归
函数的定义,并充分利⽤这个函数的返回值。
3、⽆论使⽤哪⼀种思维模式,你都要明⽩⼆叉树的每⼀个节点需要做什么,需要在什么时候(前中后序)
框架:
正序打印一个链表
struct ListNode
{
int val;
struct ListNode *next;
};
void preListNode(struct ListNode *root)
{
if(root==NULL) return;
printf(root->val); //前序位置
preListNode(root->next);
}
打印前序二叉树
struct TreeNode
{
int val;
struct TreeNode *left;
struct TreeNode *right;
};
void preTreeNode(struct TreeNode *root)
{
if(root==NULL) return;
printf(root->val);
preTreeNode(root->left);
preTreeNode(root->right);
}
后序遍历:后序位置的代码在刚离开一个(二叉树)结点时执行
struct ListNode
{
int val;
struct ListNode *next;
};
void postListNode(struct ListNode *root)
{
if(root==NULL) return;
postListNode(root->next);
printf(root->val);
}
打印后序二叉树
struct TreeNode
{
int val;
struct TreeNode *left;
struct TreeNode *right;
};
void postTreeNode(struct TreeNode *root)
{
if(root==NULL) return;
postTreeNode(root->left);
postTreeNode(root->right);
printf(root->val);
}
中序遍历:中序位置的代码在一个二叉树结点遍历完左子树,即将开始遍历右子树时执行
struct TreeNode
{
int val;
struct TreeNode *left;
struct TreeNode *right;
};
void infixTreeNode(struct TreeNode *root)
{
if(root==NULL) return;
infixTreeNode(root->left);
printf(root->val);
infixTreeNode(root->right);
}
示例 1:
输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [0]
输出:[0]
提示:
树中结点数在范围 [0, 2000] 内
-100 <= Node.val <= 100
进阶:你可以使用原地算法(O(1) 额外空间)展开这棵树吗?
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* struct TreeNode *left;
* struct TreeNode *right;
* };
*/
//采用分解法
void flatten(struct TreeNode* root){
if(root==NULL) return;
//从底向上,后序遍历
flatten(root->left);
flatten(root->right);
//开始将左右子树拉直
struct TreeNode* left=root->left;
struct TreeNode* right=root->right;
//将左子树变成右子树
root->left=NULL;
root->right=left;
//将原先的右子树连接到左子树上
struct TreeNode* h=root;
struct TreeNode* p;
while(h!=NULL)//h用来退出循环,p指向最后一个结点
{
p=h;
h=h->right;
}
p->right=right;
}
//核心:伸直树的方向从下到上,连接树的方向从上到下。
个人觉得递归的本质就是不断地合成然后再分解。
代码框架分析:
/*******二叉树遍历代码*******/
void traversal(struct TreeNode *root)
{
if(root==NULL) return;
//前序位置
traversal(root->left);
//中序位置
traversal(root->right);
//后序位置
}
/*******二叉树最大深度*******/
遍历角度:
int depth=0;
int res=0;//记录最大深度
struct TreeNode
{
int val;
struct TreeNode *left;
struct TreeNode *right;
};
void traverse(struct TreeNode *root)
{
if(root==NULL)
{
res=res>depth?res:depth;
return;
}
depth++;
traverse(root->left);
traverse(root->right);
depth--;
}
int depthBinaryTree(struct TreeNode *root)
{
traverse(root);
return res;
}
分解角度:
int maxtree(struct TreeNode *root)
{
if(root==NULL) return 0;
int leftTree=maxtree(root->left);
int rightTree=maxtree(root->right);
return max(leftTree,rightTree)+1;
}
前序遍历只能从函数参数中获取父节点传来的数据,后序遍历不仅可以获取父节点数据,还可以获取子函数(子树)的返回值传来的数据。
1.把根节点看作第一层,打印每个结点的层数
struct TreeNode
{
int val;
struct TreeNode *left;
struct TreeNode *right;
};
void traverse(struct TreeNode *root,int tier)
{
if(root==NULL) return;
printf("结点%d在%d层",root->val,tier);
traverse(root->left,tier+1);
traverse(root->right,tier+1);
}
void main(struct TreeNode *root)
{
traverse(root,1);
}
2.如何答应出每个结点的左右子树的结点数
int leftNode=0;
int rightNode=0;
struct TreeNode
{
int val;
struct TreeNode *left;
struct TreeNode *right;
};
int CountTreeNode(struct TreeNode *root)
{
if(root==NULL) return 0;
leftNode=CountTreeNode(root->left);
rightNode=CountTreeNode(root->right);
return leftNode+rightNode+1;
}
进阶框架:
求二叉树的最大半径
int maxtree=0;
int maxRadius(struct TreeNode *root)
{
if(root==NULL) return;
int leftmax=maxRadius()
void main(struct TreeNode *root)
{
traverse(root);
return maxtree;
}
/*******归并排序*******/
定义:可以理解是一棵二叉树,数组中的元素就是树的叶子结点
vector<int>temp;//定义一个temp容器
void match(vector<int>&nums,int first,int mid,int rear)
{
//采用(同向)双指针
int left=first,right=mid+1;
for(int p=first;p<=rear;p++)
{
if(left==mid+1) nums[p]=temp[right++]; //说明左半边数组排序完成
else if(right==rear+1) nums[p]=temp[left++]; //说明右半边数组排序完成
else if(nums[left]>nums[right]) nums[p]=temp[right++]; //左右半边数组开始比较
else nums[p]=temp[left++];
}
}
void sort(vector<int>&nums,int first,int rear) //进行二叉树的后序遍历
{
if(first==rear) return; //单个元素不用排序
int mid=first+(rear-first)/2;
sort(nums,0,mid);
sort(nums,mid+1,rear);
match(nums,first,mid,rear);
}
vector<int>sort_Array(vector<int>&nums)
{
for(int i=0;i<nums.size;i++)
{
temp[i]=nums[i];
}
sort(nums,0,nums.size()-1);
return nums;
}
快速排序
原理:对于一个nums[lo,...,hi]数组,先找到一个分界点po,使得nums[lo,...,po-1]中的所有元素都小于nums[po],使得nums[po+1,...,hi]中的所有所有元素都大于nums[po],然后再分别递归nums[lo,...,po-1],nums[po+1,hi]数组,使得nums[lo,...,hi]有序。
框架: //说白了就是一个前序遍历
void swap(vector<int>&nums,int first,int rear)
{
int temp=nums[rear];
nums[rear]=nums[first];
nums[first]=temp;
}
void key(vector<int>&nums,int first,int rear)
{
int number=nums[first];
int i=first+1,j=rear;
while(i<j)
{
while(i<j && number>nums[i]) {i++;}
while(j>i && number<nums[j]) {j--;}
swap(nums,i,j);
}
swap(nums,first,j);
return j;
}
vector<int>quick_sort(vector<int>&nums)
{
if(lo>=hi) return;
int q=key(nums,first,rear); //分为左右哨兵
quick_sort(nums,first,q-1);
quick_sort(nums,q+1,rear);
return nums;
}
二叉搜索树
定义:每个结点都有左右子树,左子树<结点,右子树>结点,中序遍历。
框架:
搜索元素
初级框架1:遍历二叉树法
void inBST(struct TreeNode* root)
{
if(root==NULL) return;
inBST(root->left);
//中序操作
inBST(root->right);
}
初级框架2:采用二分思想
void BST(struct TreeNode* root,int target)
{
if(root->val==target) return target;
else if(root->val <target) BST(root->right);
else BST(root->left);
}
判断是否为二叉搜索树
bool isvalidBST(struct TreeNode* root,struct TreeNode* min,struct TreeNode* max)
{
if(root==NULL) return true;
if(root->val<min->val) return false;
if(root->val > max->val) return false;
return isvalidBST(root->left,min,root)&&isvalidBST(root->right,root,max);
}
bool isBST(struct TreeNode* root)
{
//利用辅助函数加限制条件
return isvalidBST(root,null,null);
}
插入操作
struct TreeNode* InsertBST(struct TreeNode* root,int val)
{
//遇到空指针就找到指定位置
if(root==NULL) return new struct TreeNode*(val);
//寻找右子树
if(root->val < val) root->right=InsertBST(root->right,val);
//寻找左子树
if(root->val > val) root->left=InsertBST(root->left,val);
return root;
}
删除操作
struct TreeNode* DeleteBST(struct TreeNode* root,int val)
{
if(root->val==val)
//删除操作
if(root->val < val) root->right=DeleteBST(root->right,val);
if(root->val > val) root->left=DeleteBST(root->left,val);
return root;
}
分成三种情况:设A为删除的结点
1.A无左右子树
if(root->left==NULL && root->right==NULL) return;
2.A只有一个结点
if(root->left==NULL) return root->right;
if(root->right==NULL) return root->left;
3.A有两个结点
方法:找到左子树的最大结点或右子树的最小结点替换本结点
if(root->left!=NULL && root->right!=NULL)
{
struct TreeNode* temp=getMin(root->right);
root->val=temp->val;
Delete(temp);
}
总体框架:
struct TreeNode* getMin(struct TreeNode* root)
{
//左子树最小
while(root->left != NULL) root=root->left;
return root;
}
struct TreeNode* DeleteNode(struct TreeNode* root,int ret)
{
if(root==NULL) return NULL;
if(root->val==ret)
{
//解决第一种情况
if(root->left==NULL&&root->right==NULL) return NULL;
//解决第二种情况
if(root->left==NULL) return root->right;
if(root->right==NULL) return root->left;
//解决第三种情况,找到右子树的最小结点
struct TreeNode* minNode=getMin(root->right);
//删除右子树的最小结点root->
root->right=DeleteNode(root->right,minNode->val);
//替换root
minNode->left=root->left;
minNode->right=root->right;
root=minNode;
}else if(root->val > ret) root->left=DeleteNode(root->left,ret);
else if(root->val < ret) root->right=DeleteNode(root->right,ret);
}
之后会出中级篇,希望大家多多支持啦 ^0^