算法刷题笔记(初级篇)

前序:感谢学长的资料推荐以及某位算法大佬的笔记。学了数据结构和算法之后,加上一门好的编程语言刷题会达到事倍功半的效果,例如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);

}

双指针技巧

目的:解决链表和数组问题

分类:

快慢指针:一快一慢,同向而行

框架:26. 删除有序数组中的重复项

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^

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
"Labuladong"是一个著名的算法解博主,他的笔记非常受欢迎。他的笔记具有以下几个特点: 1. 思路清晰:Labuladong的笔记总是能够很清晰地阐述解思路。他善于将复杂的问简化为易于理解的小问,并通过逐步引入关键概念和方法,帮助读者理解并掌握解思路。 2. 逻辑严谨:Labuladong的笔记经过深思熟虑,逻辑严谨。他会从问的定义开始,逐步引入相关的概念和解思路,循序渐进地解决问。这种严谨的逻辑结构有助于读者理解和消化算法的核心思想。 3. 举例详细:Labuladong的笔记通常会通过具体的例子来说明解思路。这种举例的方式不仅能够帮助读者更好地理解解方法,还可以帮助读者更好地应用这些方法解决其他类似的问。 4. 知识点整合:Labuladong的笔记不仅仅是一个解,而是将相关的算法知识点整合起来,构建出一个完整的学习体系。他会引入一些底层的算法原理,将不同的解方法进行比较和总结。这种整合的方式能够帮助读者更好地理解和掌握算法的本质。 总之,Labuladong的笔记以其思路清晰、逻辑严谨、举例详细和知识点整合等特点,为广大读者提供了一种深入学习和理解算法的有效途径。通过阅读他的笔记并进行实践,读者能够提高解能力,并在面对各种算法时能够找到正确、高效的解决方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值