LeetCode算法笔记

图论

图的节点类和边类定义及转接函数

图类
class Graph{
public:
    unordered_map<int ,Node>nodes;//图的点集 键值对为<点的编号,点的具体结构>
    unordered_map<Edge>edges;//图的边集
    Graph(){
        nodes=new unordered_map<>();
        edges=new unordered_map<>();
    }
};
//边类
class Edge{
    int weight;//边的权值
    Node from;//起点
    Node to;//终点
    Edge(int weight,Node from,Node to)
    {
        this.weight=weight;
        this.from=from;
        this.to=to;
    }
};
//节点类
class Node{
    int value;//节点上面的值
    int in;//入度
    int out;//出度
    list<Node>nexts;//邻接点
    list<Edge>edges;//属于该节点的边
    Node(int value){
        this.value=value;
        in=0;
        out=0;
        nexts=new list<>();
        edges=new lists<>();
    }
};
​
//对于有向图
//matrix 所有的边
//N*3的矩阵
//[weight,from节点上面的值,to节点上面的值]
Graph createGraph(vector<vector<int>matrix)
{
    Graph graph=new Graph();
    for(int i=0;i<matrix.size();i++)
    {
        int from=matrix[i][0];//起始节点上面的数
        int to=matrix[i][1];///终止节点上面的数
        int weight=matrix[i][2];//边的权值
        
        //如果图中不包括这个节点的话,就构建这个节点并加入到图的节点集
        if(!graph.nodes.count(from))graph.nodes.emplace(from,new Node(from));
        if(!graph.nodes.count(to))graph.nodes.emplace(from,new Node(to));
        
        Node fromNode=graph.nodes[from];//获得字面值为from的节点
        Node toNode=graph.nodes[to];//获得字面值为to的节点
        Edge newEdge=new Edge(weight,fromNode,toNode);//以节点和边的权值构建一条边
        
        fromNode.nexts.emplace(toNode);//将toNode放入fromNode的邻接节点中
        fromNode.out++;//起始节点出度加一
        toNode.in++;//终止节点入度加一
        fromNode.edges.emplace(newEdges);//将边加入起始节点的边集 
        graph.endges.emplace(newEdges);将边加入图的边集
    }
        return graph;
}

图的广度优先遍历

1、利用队列实现

2、从源节点开始依次按照宽度进队列,然后弹出

3、每弹出一个点,就把该节点所有没有进过队列的邻接点放入队列

4、直到队列变空

vector<int>ans;
void bfs(Node node)
{
    if(nod==NULL)return ;
    queue<Node>que;
    unordered_set<Node>hashset;//记录已经访问过的点
    que.push(node);//将源节点放入队列
    hashset.emplace(node);
    while(!que.empty())
    {
        Node cur=que.front();
        que.pop();
        ans.push_back(cur->val);
        for(Node next:cur.nexts)//将队头节点的所有不在哈希表中的邻接节点放入队列
        {
            if(!hashset.count(next))
            {
                hashset.emplace(next);
                queue.emplace(next);
            }
        }
    }
}

图的深度优先遍历

1、利用栈实现

2、从源节点开始把节点按照深度放入栈,然后弹出

3、每弹出一个点,就把该节点下一个没有进过栈的邻接点放入栈

4、直到栈变空

vector<int>ans;
void dfs(Node node)
{
    if(node==NULL)return ;
    stack<Node>st;
    unordered_set<Node>hashset;//记录已经访问过的节点
    st.push(node);
    hashset.emplace(node);
    ans.push_back(node->val);//处理队头元素
    while(!st.empty())
    {
        Node cur=st.top();
        st.pop();
        for(Node next:cur.nexts)//遍历栈头节点的邻接节点
        {
            if(!st.count(next))
            {
                st.push(cur);//将弹出的节点再入栈
                st.push(next);//将其邻接节点入栈
                hashset.emplace(next);
                ans.push_back(next->val);//处理新入栈的元素
                break;//选择其中一条路径一直往下走
            }
        }
    }
}

例题

lc399:计算除法

思想:

1、首先用一个哈希表记录字符串的数量,键值对为<字符串,出现的序号>

2、构建边集,边集下标为分子字符串的序号,元素为与分子字符串有直接除法关系的分母字符串及比值构成的键值对组成的vector

3、遍历问题数组queries[],取出其元素的下标,如字符串下标相等直接返回1,不相等的话,构建一个点集队列,第一个元素进入队列,访问第一个元素的边集,找出与其有直接关系字符串并计算比值加入队列,直到找到第二个元素

1、广度优先搜索

class Solution {
public:
    vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
        int nvars = 0;//记录equations中不同的字符串数量
        unordered_map<string, int> variables;//记录字符串及其在equations中出现的次序,从0开始
​
        int n = equations.size();
        for (int i = 0; i < n; i++) {
            if (variables.find(equations[i][0]) == variables.end()) {
                variables[equations[i][0]] = nvars++;
            }
            if (variables.find(equations[i][1]) == variables.end()) {
                variables[equations[i][1]] = nvars++;
            }
        }
​
        // 对于每个点,存储其直接连接到的所有点及对应的权值
        vector<vector<pair<int, double>>> edges(nvars);//edges[i]为一个与第i个字符串有直接关系的字符串的对组的数组
        for (int i = 0; i < n; i++) {
            int va = variables[equations[i][0]], vb = variables[equations[i][1]];
            edges[va].push_back(make_pair(vb, values[i]));
            edges[vb].push_back(make_pair(va, 1.0 / values[i]));
        }
​
        vector<double> ret;
        for (const auto& q: queries) 
        {
            double result = -1.0;
            if (variables.find(q[0]) != variables.end() && variables.find(q[1]) != variables.end())//只有两个字符串都存在时,才能求比值
            {
                int ia = variables[q[0]], ib = variables[q[1]];//取得两个字符串对应的下标
                if (ia == ib) {//两下标相等表示字符串相等
                    result = 1.0;
                } 
                else 
                {
                    queue<int> points;
                    points.push(ia);
                    vector<double> ratios(nvars, -1.0);
                    ratios[ia] = 1.0;
​
                    while (!points.empty() && ratios[ib] < 0) {//队列不空时,找到中间字符串的序号加入队列,直到找到目标字符串对应的序号
                        int x = points.front();
                        points.pop();
​
                        for (const auto [y, val]: edges[x]) {//遍历与队头元素有直接除数关系的对组
                            if (ratios[y] < 0) {//如果字符串序号不在队列中
                                ratios[y] = ratios[x] * val;//保存与被除数字符串元素的比值
                                points.push(y);
                            }
                        }
                    }
                    result = ratios[ib];
                }
            }
            ret.push_back(result);
        }
        return ret;
    }
};
2、Floyd算法

class Solution {
public:
    vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
        int nvars = 0;
        unordered_map<string, int> variables;
​
        int n = equations.size();
        for (int i = 0; i < n; i++) {
            if (variables.find(equations[i][0]) == variables.end()) {
                variables[equations[i][0]] = nvars++;
            }
            if (variables.find(equations[i][1]) == variables.end()) {
                variables[equations[i][1]] = nvars++;
            }
        }
        vector<vector<double>> graph(nvars, vector<double>(nvars, -1.0));
        for (int i = 0; i < n; i++) {
            int va = variables[equations[i][0]], vb = variables[equations[i][1]];
            graph[va][vb] = values[i];
            graph[vb][va] = 1.0 / values[i];
        }
​
        for (int k = 0; k < nvars; k++) {
            for (int i = 0; i < nvars; i++) {
                for (int j = 0; j < nvars; j++) {
                    if (graph[i][k] > 0 && graph[k][j] > 0) {
                        graph[i][j] = graph[i][k] * graph[k][j];
                    }
                }
            }
        }
​
        vector<double> ret;
        for (const auto& q: queries) {
            double result = -1.0;
            if (variables.find(q[0]) != variables.end() && variables.find(q[1]) != variables.end()) {
                int ia = variables[q[0]], ib = variables[q[1]];
                if (graph[ia][ib] > 0) {
                    result = graph[ia][ib];
                }
            }
            ret.push_back(result);
        }
        return ret;
    }
};

拓扑排序

适用范围:要求有向图,且有入度为0的节点,且没有环

实现的逻辑就是:先找出入度为0的节点,然后把他们打印,并且打印完成之后就马上删除掉这个点的影响,即把与其相连的节点的入度都减一,并将减一后入度为0的节点进入 “0入度队列”,然后从“0入度队列”弹出一个入度为0的节点,继续前边的操作,直到图被完全遍历

List<Node> sortedTopology(graph graph)
{
    unordered_map<Node,int>hash;//key--某一个node,value--剩余的入度
    queue<Node>zeroInQueue;//保存入度为0的点
    for(Node node:graph.nodes.values())
    {
        hash.emplace(node,node.in);//将 <节点,节点入度> 键值对放入哈希表
        if(node.in==0)zeroInQueue.push(node);//将入度为0的点放入队列
    }
    //拓扑排序的结果放入result
    queue<Node>result;
    while(!zeroInQueue.empty())
    {
        Node cur=zeroInQueue.front();
        zeroInQueue.pop();
        result.emplace(cur);
        for(Node next:cur.nexts)//循环擦除cur的影响
        {
            hash[next]=hash[next]-1;
            if(hash[next]==0)//节点入度减一后为0
            zeroInQueue.push(next);
        }
    }
    return result;
}

并查集

1、概论

定义:并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。

主要构成:并查集主要由一个整型数组pre[ ]和两个函数find( )、join( )构成。数组 pre[ ] 记录了每个点的前驱节点是谁,函数 find(x) 用于查找指定节点 x 属于哪个集合,函数 join(x,y) 用于合并两个节点 x 和 y 。

作用:

并查集的主要作用是求连通分支数(如果一个图中所有点都存在可达关系(直接或间接相连),则此图的连通分支数为1;如果此图有两大子图各自全部可达,则此图的连通分支数为2……)

即:用集合中的某个元素来代表这个集合,则该元素称为此集合的代表元。

2、find()函数的意义与实现

首先需要定义一个数组parent[]这个数组,parent[i]中i表示哪个节点,parent[i]表示它的上级,find()函数是找到它的给定节点所属集合的代表元,代表元的parent[i]等于i,即代表元没有上级

实现:

int find(int x)
{
    while(parent[x]!=x){
        x=parent[x];
    }
    return x;
}

3、union()函数的意义和实现

union()函数的作用是为了将给定的两个节点所属集合合并起来

union()函数的执行逻辑如下:

1、找到节点x所属集合的代表元

2、找到节点y所属集合的代表元

3、如果 x 和 y 不相等,则随便选一个人作为另一个人的上级,如此一来就完成了 x 和 y 的合并。

union()函数实现:

void union(int x,int y)
{
    int parentX=find(x),parentY=find(y);
    if(parentX!=parentY)parent[parentX]=parentY;
}

4、路径压缩算法一:优化find()函数

由于逐层向上查找太浪费时间,这种树的深度越高,查找速度越慢,所以可以将一个集合中的所有节点都挂靠在代表元之下

实现:

int find(int x)
{
    if(parent[x]==x)return x;
    //只有查找了一个不是代表元的节点之后,才会自动进行状态压缩
    return parent[x]=find[parent[x]];//路径压缩,将x挂靠在find(parent[x])之下
}

该算法存在一个缺陷:只有当查找了某个节点的代表元(教主)后,才能对该查找路径上的各节点进行路径压缩。换言之,第一次执行查找操作的时候是实现没有压缩效果的,只有在之后才有效。 因为第一次查找时需要借助find()递归的构建高度为2的关系树。

5、路径压缩算法:加权标记法

3、union()函数的意义和实现中所示的union()函数很可能会形成一字长蛇型的树结构,对于查询非常不利,所以可以先获得将给定节点x、y所属集合的高度,将高度低的集合挂靠在高度高的集合下面

主要思路:加权标记法需要将树中所有节点都增设一个权值,用以表示该节点所在树中的高度(比如用rank[x]=3表示 x 节点所在树的高度为3)。这样一来,在合并操作的时候就能通过这个权值的大小来决定谁当谁的上级(玄慈哭了:“正义终会来到,但永不会缺席”)。在合并操作的时候,假设需要合并的两个集合的代表元(教主)分别为 x 和 y,则只需要令pre[x] = y 或者pre[y] = x 即可。但我们为了使合并后的树不产生退化(即:使树中左右子树的深度差尽可能小),那么对于每一个元素 x ,增设一个rank[x]数组,用以表达子树 x 的高度。在合并时,如果rank[x] < rank[y],则令pre[x] = y;否则令pre[y] = x。

实现:加权标记法的核心在于对rank数组的逻辑控制,其主要的情况有:1、如果rank[x] < rank[y],则令pre[x] = y;2、如果rank[x] == rank[y],则可任意指定上级;3、如果rank[x] > rank[y],则令pre[y] = x;在实际写代码时,为了使代码尽可能简洁,我们可以将第1点单独作为一个逻辑选择,然后将2、3点作为另一个选择(反正第2点任意指定上级嘛),所以具体的代码如下:

void union(int x,int y)
{
    x=find(x);      //寻找 x的代表元
    y=find(y);      //寻找 y的代表元
    if(x==y) return ;//如果 x和 y的代表元一致,说明他们共属同一集合,则不需要合并,直接返回;否则,执行下面的逻辑
    if(rank[x]>rank[y]) pre[y]=x;//如果 x的高度大于 y,则令 y的上级为 x
    else                                //否则
    {
        if(rank[x]==rank[y]) rank[y]++; //如果 x的高度和 y的高度相同,则令 y的高度加1
        pre[x]=y;                   //让 x的上级为 y
    }
}

6、总结

1、用集合中的某个元素来代表这个集合,则该元素称为此集合的代表元;2 、一个集合内的所有元素组织成以代表元为根的树形结构;3 、对于每一个元素 x,pre[x] 存放 x 在树形结构中的父亲节点(如果 x 是根节点,则令pre[x] = x);4 、对于查找操作,假设需要确定 x 所在的的集合,也就是确定集合的代表元。可以沿着pre[x]不断在树形结构中向上移动,直到到达根节点。因此,基于这样的特性,并查集的主要用途有以下两点:1、维护无向图的连通性(判断两个点是否在同一连通块内,或增加一条边后是否会产生环);2、用在求解最小生成树的Kruskal算法里。

const int  N=1005   //指定并查集所能包含元素的个数(由题意决定)
int pre[N];         //存储每个结点的前驱结点 
int rank[N];        //树的高度 
void init(int n)     //初始化函数,对录入的 n个结点进行初始化 
{
    for(int i = 0; i < n; i++){
        pre[i] = i;     //每个结点的上级都是自己 
        rank[i] = 1;    //每个结点构成的树的高度为 1 
    } 
}
int find(int x)         //查找结点 x的根结点 
{
    if(pre[x] == x) return x;   //递归出口:x的上级为 x本身,则 x为根结点 
    return find(pre[x]);        //递归查找 
} 
 
int find(int x)     //改进查找算法:完成路径压缩,将 x的上级直接变为根结点,那么树的高度就会大大降低 
{
    if(pre[x] == x) return x;   //递归出口:x的上级为 x本身,即 x为根结点 
    return pre[x] = find(pre[x]);  //此代码相当于先找到根结点 rootx,然后 pre[x]=rootx 
} 
​
bool isSame(int x, int y)       //判断两个结点是否连通 
{
    return find(x) == find(y);  //判断两个结点的根结点(即代表元)是否相同 
}
​
bool join(int x,int y)
{
    x = find(x);                    //寻找 x的代表元
    y = find(y);                    //寻找 y的代表元
    if(x == y) return false;//如果 x和 y的代表元一致,说明他们共属同一集合,则不需要合并,返回 false,表示合并失败;否则,执行下面的逻辑
    if(rank[x] > rank[y]) pre[y]=x; //如果 x的高度大于 y,则令 y的上级为 x
    else                                //否则
    {
        if(rank[x]==rank[y]) rank[y]++; //如果 x的高度和 y的高度相同,则令 y的高度加1
        pre[x]=y;                       //让 x的上级为 y
    }
    return true;                        //返回 true,表示合并成功
}
左氏代码

#include <stdio.h>
#include <stdlib.h>
#define SIZE 6//定义最大节点数量
void init(int parent[])//初始化并查集
{
    for(int i=0;i<6;i++)
    {
        parent[i]=-1;
    }
}
int find_root(int x,int parent[])//找到父节点
{
    int x_root=x;
    while(parent[x_root]!=-1)x_root=parent[x_root];
    return x_root;
}
bool union(int x,int y,int parent[])//合并两个集合
{
    int x_root=find_root(x,parent);
    int y_root=find_root(y,parent);
    if(x_root==y_root)return false;//两个节点的最终父节点相同,则说明两个节点在同一个集合,合并失败
    int temp=parent[x_root]+parent[y_root];//计算合并后元素个数
    if(parent[x_root]<parent[y_root])//x集合的元素比较多,则将y集合合并到x集合
    {
        parent[y]=x_root;
        parent[x_root]=temp;
    }
    else
    {
        parent[x]=y_root;
        parent[y_root]=temp
    }
    return true;
}
int main()
{
    int parent[6];
    int edges[6][2]={{0,1},{1,2},{1,3},{2,4},{3,4},{2,5}};
    init(parent);
    for(int i=0;i<SIZE;i++)
    {
        int x=edges[i][0];
        int y=edges[i][1];
        if(!union(x,y,parent)){
            printf("Cycle detected! \n");
            exit(1);
        }
    }
    printf("No find cycle \n");
    return 0;
}

最小生成树以及相关算法

关于图的几个概念定义:

连通图:在无向图中,若任意两个顶点vi与vj都有路径相通,则称该无向图为连通图。强连通图:在有向图中,若任意两个顶点vi与vj都有路径相通,则称该有向图为强连通图。连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。

1、Kruskal算法:要求无向图

思想:首先将各个边的权值排序,从最小的边开始,判断加入最小的边是否会生成环

如果不生成则加入,如果生成环就不要

使用集合查询和集合合并的方式来判断是否会生成环,假如一开始图中各个节点是独立的集合,

遍历边的权值的集合,判断边的出点和入点是否在同一集合,如果不在则说明不存在环,将两个点的集合合并

如果在同一个集合,则说明加入此边会形成环,则放弃这条边

例如:点集合为{A}、{B}、{C}、{D}、{E}、{F},边集合为{A--2--B,C--3--D,D--5--E ....}

加入第一条边A、B不在同一个集合中,则合并A、B为{A、B}....依次遍历

实现并查集功能创建最小生成树

class UnionFind{
public:
    unordered_map<Node,list<Node>>hashMap;//哈希表存储节点以及节点所在集合
    UnionFind(list<Node>nodes)//构造函数
    {
        for(Node node:nodes)//初始化点集合
        {
            list<Node>que;
            que.emplace(node);//每个点为独立集合
            hashMap.emplace(node,que);
        }
    }
    //判断两个节点是否属于一个集合
    bool isSameSet(Node from,Node to)
    {
        list<Node>fromSet=hashMap[from];
        list<Node>toSet=hashMap[to];
        return fromSet==toSet;
    }
    //合并两个集合
    void union(Node from,Node to)
    {
        list<Node>fromSet=hashMap[from];
        list<Node>toSet=hashMap[to];
        for(Node toNode:toSet)
        {
            fromSet.add(toNode);//将toSet集合中的点放入fromSet集合中
            hashMap.emplace(toNode,fromSet)//更改哈希表中节点对应的集合
        }
    }
}
//创建一个比较函数,比较边的权值
bool compareEdge(Edge l1,Edge l2)
{
    return l1.weight<l2.weight;
}
​
list<Edge> KruskalMST(Graph graph)
{
    UnionFind unionFind;
    unionFind.makeSets(graph.nodes.values);//初始化并查集
    priority_queue<Edge,vector<Edge>,compareEdge())que;//小根堆保存边
    for(Edge edge:graph.edges)
    {
        que.emplace(edge);
    }
    list<Edge>result;
    while(!que.empty())
    {
        Edge edge=que.front();
        if(!unionFind.isSameSet(edge.from,edge.to))
        {
            result.emplace(edge);
            unionFind.union(edge.from,edge.to);
        }
    }
    return result;
}
​

2、prim算法:要求无向图

思想:找到一个开始的点,判断是否在哈希表中,不在的话就加入哈希表,由这个点解锁所有相连的边并加入队列,弹出队列中所解锁的最小边,如果这个最小边的终点不在哈希表中,就加入哈希表,并解锁其最小边并加入队列

list<Edge> primMST(graph graph)
{
    priority_queue<Edge,vector<Edge>,compareEdge()>que;
    unordered_set<Node>hashSet;
    list<Edge>result;
    for(Node node:graph.nodes.values()){//随便选择一个点
        if(!hashSet.count(node)){
            hashSet.emplace(node);
            result.emplace(node);
            for(Edge edge:node.edges){//解锁该点邻接的所有的边
                que.emplace(edge);
            }
        }
        while(!que.empty())
        {
            Edge edge=que.front();//弹出解锁的边中权值最小的边
            que.pop();
            Node toNode=edge.to;
            if(!hashSet.count(toNode)){
                hashSet.emplace(toNode);
                result.emplace(toNode);
                for(Edge edge:node.edges){//解锁该点邻接的所有的边
                que.emplace(edge);
                }
            }
        } 
    }
    return result;
}

3、Dijkstra算法(最短路径算法)

适用范围:没有权值为负数的边

最短路径问题的提法为:给定一个有向带权图D和源点v,各边上的权值均非负,要求找出从v到D中其他各顶点的最短路径

思想:首先构建一个哈希表,里面存入<head,0>,键值对为当前节点、源节点到当前节点的最小距离值,取出哈希表中距离最小的值的节点,找到它的邻接节点,如果当前节点的距离值加上它拥有边的权值,能使其邻接节点到源节点的值减小,则更新其值

unordered_map<Node,int>dijkstra1(Node head)
{
    //从head出发到所有点的最小距离
    //从head出发到达key
    //value:从head出发到达key的最小距离
    //如果哈希表中没有T的记录,含义是从head出发到T这个点的距离为正无穷
    unordered_map<Node,int>distanceMap;
    
    //已经求过距离的点,存在selectNodes中,以后再也不碰
    unordered_set<Node>selectNodes;
    
    //找到distanceMap中从源节点到当前节点最小的距离的节点,且节点不在selectNodes中
    Node minNode=getMinDisAndUnsel(distanceMAp,selectNodes);//获取起始点
    
    while(minNode!=NULL)
    {
        int distanceMap=distanceMap[minNode];//获取最小距离
        for(Edge edge:minNode.edges)//遍历该节点的邻接节点并更新邻接节点到起点的距离值
        {
            Node toNode=edge.to;//获取每条边的指向节点,即该节点的邻接节点
            
            //如果没解锁过,插入distanceMap
            if(!distanceMap.count(toNode))
                distanceMap.emplace(toNode,distance+edge.weight);
            else  //解锁过就更新其距离使其当前最小
                distanceMap[toNode]=min(distanceMap[toNode],distance+edge.weight);
        }
        selectNodes.emplace(minNode);//将这个点锁定以后不在使用
        minNode=getMinDisAndUnsel(distanceMAp,selectNodes);
    }
}
//找到集合distanceMap中距离起点最近的节点
Node getMinDisAndUnsel(unordered_map<Node,int>&distanceMap,unordered_set<Node>&selectNodes)
{
    Node temp=distanceMap.begin()->first;//节点
    int len=distanceMap.begin()->second;//距离起点的值
    for(auto&p:distanceMap)
    {
        if(p.second<len&&!selectNodes.count(p.first))temp=p.first;
    }
    return temp;
}
 

树、二叉树、二叉搜索树、哈夫曼树

递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。

递归,就是在运行的过程中调用自己。构成递归需具备的条件:

1. 子问题须与原始问题为同样的事,且更为简单;

2. 不能无限制地调用本身,须有个出口,化简为非递归状况处理

迭代原理:利用变量的原值推算出变量的一个新值.如果递归是自己调用自己的话,迭代就是A不停的调用B.

递归中一定有迭代,但是迭代中不一定有递归,大部分可以相互转换.能用迭代的不用递归,递归调用函数,浪费空间,并且递归太深容易造成堆栈的溢出.

二叉树的遍历

前序遍历:中左右

递归解法

vector<int>ans;
void preorder(TreeNode*root)
{
    ans.push_back(root->val);
    preorder(root->left);
    preorder(root->right);
}

迭代解法(辅助栈)

//借用辅助栈时,前序遍历顺序为中左右,借用辅助栈的话,中先出栈,右进栈,然后左进栈,这样出栈时顺序才为中左右
vector<int> preorder(TreeNode*root)
{
    if(!root)return {};
    vector<int>ans;
    stack<TreeNode*>st;
    st.push(root);
    while(!st.empty())
    {
        TreeNode*node=st.top();
        st.pop();
        ans.push(node->val);
        if(root->right)st.push(root->right);
        if(root->left)st.push(root->left);
    }
    return ans;
}
中序遍历:左中右

递归解法

vector<int>ans;
void inorder(TreeNode*root)
{
    preorder(root->left);
    ans.push_back(root->val);
    preorder(root->right);
}

迭代解法

中序遍历的迭代解法与前序遍历的迭代解法不同,中序遍历为左中右,则需要首先找到最左边的元素,从最左边的元素开始处理

vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while (cur != NULL || !st.empty()) {
            if (cur != NULL) { 
            	// 指针来访问节点,访问到最底层
                st.push(cur); // 将访问的节点放进栈
                cur = cur->left;                // 左
            } 
            else {
            	//从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
                cur = st.top(); 
                st.pop();
                result.push_back(cur->val);     // 中
                cur = cur->right;               // 右
            }
        }
        return result;
    }
后序遍历:左右中

递归解法

vector<int>ans;
void inorder(TreeNode*root)
{
    preorder(root->left);
    preorder(root->right);
    ans.push_back(root->val);
}

迭代解法(辅助栈)

//借用辅助栈时,前序遍历顺序为中左右,借用辅助栈的话,中先出栈,右进栈,然后左进栈,这样出栈时顺序为中左右,此时更改入栈顺序,左入栈,然后右入栈,此时出栈顺序为中右左,再翻转一下,即为后序遍历左右中
vector<int> preorder(TreeNode*root)
{
	if(!root)return {};
	vector<int>ans;
    stack<TreeNode*>st;
    st.push(root);
    while(!st.empty())
    {
        TreeNode*node=st.top();
        st.pop();
        ans.push(node->val);
        if(root->left)st.push(root->left);
        if(root->right)st.push(root->right);
    }
    return ans;
}
二叉树最大深度
三种解法

1、递归解法

int height(TreeNode*root)
{
    if(!root)return 0;
    int maxL=0,maxR=0,maxH=0;
    if(root->left)maxL=height(root->left);
    if(root->right)maxR=height(root->right);
    maxH=max(maxL,maxR)+1;
    return maxH;
}


2、深度优先搜索

int maxDepth(TreeNode* root) 
{
    //递归
    if(root==nullptr)
    {
        return 0;
    }
    return max(maxDepth(root->left),maxDepth(root->right))+1;
}



3、层序遍历解法

int maxDepth(TreeNode*root)
{
	if(!root)return 0;
    queue<TreeNode*>que;
    int h=0;
    que.push(root);
    while(!que.empty())
    {
        h++;
        int n=que.size();
        while(n>0)
        {
            if(que.front()->left)que.push(que.front()->left);
            if(que.front()->right)que.push(que.front()->right);
            que.pop();
        }
    }
    return h;
}
平衡二叉树
class Solution {
public:
    bool isBalanced(TreeNode* root) {
        if(!root)return true;
        int lh=height(root->left);
        int rh=height(root->right);
        return abs(lh-rh)<=1&&isBalanced(root->left)&&isBalanced(root->right);
    }
    int height(TreeNode*root){
        if(!root)return 0;
        return max(height(root->left),height(root->right))+1;
    }
};
二叉树的最小高度
递归解法

int minDepth(TreeNode*root)
{
    if(root==NULL)return 0;//针对根节点为NULL的情况
    if(!root->left&&!root->right)return 1;//遍历到第一个叶子节点时就不再向下搜索
    int minD=INT_MAX;
    if(root->left)minD=min(minD,minDepth(root->left));//向左搜索
    if(root->right)minD=min(minD,minDepth(root->right));//向右搜索
    return minD+1;
}


广度优先搜索解法

int minDepth(TreeNode*root)
{
    queue<TreeNode*>que;
    que.push(root);
    int minD=0;
    while(!que.empty())
    {
        int n=que.size();
        minD++;
        while(n>0)
        {
            TreeNode*node=que.front();
            que.pop();--n;
            if(!node->right&&!node->left)return minD;
            if(node->left)que.push(node->left);
            if(node->right)que.push(node->right);
        }
    }
    return minD;
}
翻转二叉树
层序遍历解法

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) 
    {
    	   if(root==NULL)return root;
    	   queue<TreeNode*>que;
    	   que.push(root);
    	   while(!que.empty())
    	   {
               int n=que.size();
               while(n>0)
               {
                   TreeNode*node=que.front();
                   que.pop();--n;
                   swap(node->left,node->right);
                   if(node->left)que.push(node->left);
                   if(node->right)que.push(node->right);
               }
    	   }
    	   return root;
    }
}


递归解法(前序、中序、后序)

//前序
class Solution {
public:
    TreeNode* invertTree(TreeNode* root) 
    {
    	   if(root==NULL)return root;
    	   swap(root->left,root->right);//左右子树交换
    	   invertTre(root->left);//翻转左子树
    	   invertTree(root->right);//翻转右子树
    	   return root;
    }
}

//中序
class Solution {
public:
    TreeNode* invertTree(TreeNode* root) 
    {
    	   if(root==NULL)return root;
    	   invertTre(root->left);//翻转左子树
    	   swap(root->left,root->right);//左右子树交换
    	   invertTree(root->left);//左边已经翻转过了,交换之后左边到右边,即右边翻转过了,所以此时要翻转左子树
    	   return root;
    }
}

//后序
class Solution {
public:
    TreeNode* invertTree(TreeNode* root) 
    {
    	   if(root==NULL)return root;
    	   invertTre(root->left);//翻转左子树
    	   invertTree(root->right);//翻转右子树
    	   swap(root->left,root->right);//左右子树交换
    	   return root;
    }
}
对称二叉树
递归解法

class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        return dfs(root->left,root->right);
    }
    bool dfs(TreeNode*p,TreeNode*q)
    {
        if(!p&&!q)return true;
        if(!p||!q)return false;
        if(p->val!=q->val)return false;
        return dfs(p->left,q->right)&&dfs(p->right,q->left);
    }
};


迭代解法(队列、栈)

//队列
class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        queue<TreeNode*>que;
        que.push(root->left);
        que.push(root->right);
        while(!que.empty())
        {
            TreeNode*leftNode=que.front();que.pop();
            TreeNode*rightNode=que.front();que.pop();
            if(!leftNode&&!rightNode)return true;//都为空,返回真
            if(!leftNode||!rightNode)return false;//其中一个为空,返回假
            if(leftNode->val!=rightNode->val)return false;//数值不等,返回假
            que.push(leftNode->left);
            que.push(rightNode->right);
            que.push(leftNode->right);
            que.push(rightNode->left);
        }
        return true;
    }
};

//栈
class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        satck<TreeNode*>st;
        st.push(root->left);
        st.push(root->right);
        while(!st.empty())
        {
            TreeNode*leftNode=st.top();que.pop();
            TreeNode*rightNode=st.top();que.pop();
            if(!leftNode&&!rightNode)return true;//都为空,返回真
            if(!leftNode||!rightNode)return false;//其中一个为空,返回假
            if(leftNode->val!=rightNode->val)return false;//数值不等,返回假
            st.push(leftNode->left);
            st.push(rightNode->right);
            st.push(leftNode->right);
            st.push(rightNode->left);
        }
        return true;
    }
};

leetcode 计算质数

题目:计算整数n以内的质数个数

埃氏筛法原理

我们考虑这样一个事实:如果 x 是质数,那么大于 x 的 x 的倍数 2x,3x,… 一定不是质数,因此我们可以从这里入手。

我们设isPrime[i] 表示数 i 是不是质数,如果是质数则为 1,否则为 0。从小到大遍历每个数,如果这个数为质数,则将其所有的倍数都标记为合数(除了该质数本身),即 0,这样在运行结束的时候我们即能知道质数的个数。

这种方法的正确性是比较显然的:这种方法显然不会将质数标记成合数;另一方面,当从小到大遍历到数 x 时,倘若它是合数,则它一定是某个小于 x 的质数 y 的整数倍,故根据此方法的步骤,我们在遍历到 y 时,就一定会在此时将 x标记isPrime[x]=0。因此,这种方法也不会将合数标记为质数。

当然这里还可以继续优化,对于一个质数 x,如果按上文说的我们从 2x 开始标记其实是冗余的,应该直接从 x⋅x 开始标记,因为 2x,3x,… 这些数一定在 x之前就被其他数的倍数标记过了,例如 2 的所有倍数,3 的所有倍数等

class Solution {
public:
    int countPrimes(int n) {
        vector<int> isPrime(n, 1);
        int ans = 0;
        for (int i = 2; i < n; ++i) 
        {
            if (isPrime[i]) 
            {
                ans += 1;
                if ((long long)i * i < n) 
                {
                    for (int j = i * i; j < n; j += i) 
                    {
                        isPrime[j] = 0;
                    }
                }
            }
        }
        return ans;
    }
};

背包问题

1、0-1背包

0-1背包问题描述:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

定义dp【i】【j】的含义:其表示在0-i件物品中任意取,放进容量为j的背包中,所能取得的最大价值

对于第i件物品,有两种选择,选或者不选

​ 1、将第i件物品放入背包,总价值增加value[i],且背包容量会变化,变为j-weight[i], 此时dp[i][j]的值与dp[i-1][j-weight[i]]相关,则``dp[i][j]=dp[i-1][j-weight[i]]+value[i];

​ 2、不放第i件物品,背包容量无变化,与从0-i-1件物品任意选择相同,dp[i][j]=dp[i-1][j];

dp[i][j]的初始化

首先dp[i][0]=0,因为背包容量为0,装的总价值也一定为0,其次i=0时,只能选择第一件物品,则当j<weight[0]时,dp[0][j]=0;当j>weight[0]时,dp[0][j]=value[i];

代码:

先遍历物品再遍历背包容量:每遍历一个物品,问各个容量的背包能不能装下,取的能装下的最大价值,然后在遍历下一个物品,再问一次
for(int i=1;i<n;i++)
{
    for(int j=0;j<=begweight;j++)
    {
        if(j<weight[i])dp[i][j]=dp[i-1][j];//容量不够,不装weight[i]
        else dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);//容量够的的话,在选与不选中,挑一个最大的,如果j-weight[i]足够大的话,肯定也装有weight[i-1],总之,dp[i-1][j-weight[i]]为当前背包容量能取得的最大价值
    }
}
​
先遍历背包再遍历物品:当背包容量为j时,遍历一遍物品,求得容量为j时可取得最大价值
for(int j=0;j<=begweight;j++)
{
    for(int i=1;i<n;i++)
    {
        if(j<weight[i])dp[i][j]=dp[i-1][j];
        else dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
    }
}

C++完整代码(二维)

    for (int j = weight[0]; j <= bagWeight; j++) {
        dp[0][j] = value[0];
    }
    for(int j=0;j<=begweight;j++)//遍历背包
    {
        for(int i=1;i<n;i++)//遍历物品
        {
            if(j<weight[i])dp[i][j]=dp[i-1][j];
            else dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
        }
    }
}
​

上述背包问题是可以压缩的,设dp[j]为容量为j的背包能装的最大价值

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

倒叙遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

为什么呢?因为每个物品都要遍历一遍所有的背包,在递推公式中存在dp[j-weight],当倒叙遍历的时候,能保证先遍历容量为j的背包,当遍历容量为j的背包时,不管容量为j-weight[i]的背包能不能装得下第i个物品,它目前一定没有装,这样就避免了重复将同一个元素放入背包的情况

再来看一下,正序遍历的情况,正序遍历时,容量为j-weight[i]的背包一定先访问,然后在访问容量为j的背包,这样一来,物品i可能装进j-weight[i]的背包后,又装进了容量为j的背包!

leetcode279、完全平方数

广度优先搜索

class Solution{
public:
    int numSquares(int n){
        queue<int>que;//记录每层的数
        unordered_set<int>st;//记录已经出现的平方数和
        que.push(0);
        st.insert(0);
        int level=0;
        while(!que.empty())
        {
            int size=que.size();//计算每层的个数
            level++;
            for(int i=0;i<size();i++)
            {
                int digit=que.front();
                que.pop();
                for(int j=1;j*j<=n;j++)
                {
                    int num=digit+j*j;
                    if(num==n)return level;
                    if(num>n)break;
                    if(!st.count(num)){
                        que.push(num);
                        st.insert(num);
                    }
                }
            }
        }
        return level;
    }
};

深度优先搜索(会超时)

class Solution{
public:
    int minNum=INT_MAX;
    int numSquares(int n){
        dfs(0,0,n);
        return minNum;
    }
    void dfs(int sum,int cnt,int n)
    {
        if(sum==n)minNum=min(cnt,minNum);
        else if(sum>n)return ;
        for(int i=sqrt(n);i>0;i--)
        {
            dfs(sum+i*i,cnt+1,n);
        }
    }
}

动态规划

class Solution{
public:
    int numSquares(int n) {
        //dp[i]表示金额为i时所需要的最少数量
        //则dp[i]=min(dp[i-1*1],dp[i-2*2],dp[i-3*3],,,);
        vector<int>dp(n+1);
        for(int i=1;i<=n;i++)
        {
            int minNum=INT_MAX;
            for(int j=1;j*j<=i;j++)
            {
                minNum=min(minNum,dp[i-j*j]);
            }
            dp[i]=minNum+1;
        }
        return dp[n];
    }
};

leetcode483、最小好进制

class Solution {
public:
    string smallestGoodBase(string n) {
        long num=stol(n);
        for(int i=(int)(log(num)/log(2)+1);i>2;--i)//从1的个数最多时候的二进制开始枚举
        {
            long left=2,right=num-1;//二分法查找适当的进制k,使得在s情况下等于num
            while(left<=right)
            {
                long mid=left+(right-left)/2;
                long s=0,maxVal=num/mid+1;//maxVal防止溢出
                
                //计算在mid进制下,此时的值
                for(int j=0;j<i;j++)
                {
                    if(s<maxVal)s=s*mid+1;
                    else {
                        s=num+1;
                        break;
                    }
                }
                //根据计算出的值,判断当前进制是否满足题意
                if(s==num)return to_string(mid);
                else if(s>num)right=mid-1;//说明在此进制下数值太大,需缩小进制
                else left=mid+1;
            }
        }
        return to_string(num-1);
    }
};
注意:数值类型均为long

KMP算法:快速查找匹配串的算法

1、题目:leetcode28.实现strStr();strStr(str1,str2) 函数用于判断字符串str2是否是str1的子串。如果是,则该函数返回str2在str1中首次出现的地址;否则,返回NULL。

前缀:包括首字母、不包括尾字母的子串

后缀:包括尾字母、不包括首字母的子串

思路:首先算出匹配串的每一位之前的子串的前缀和后缀的最长的相等子串,存储在next数组中,next 数组记录的就是最长相同前后缀

例如,字符串 ”aabaaf"的next数组为 next[]={ 0,1,0,1,2,0}

当匹配串与目标串匹配时,如果不相等,即p[i]!=t[j],则查找next数组的第i-1位上的数字,然后匹配串跳转到匹配串的第next[i-1]个字符开始匹配,如果还不相等则继续向前跳转匹配,直到跳转到匹配串的首个字母

例如,设目标串t[]="aabaabaaf",匹配串p[]="aabaaf",第一次匹配时,t[5]!=p[5],则查看next[4]上的值,next[4]=2,则下一次匹配从p[2]开始匹配,此时p[2]=t[5],再继续匹配...

下面来求next数组,分别用i表示后缀末尾位置,j表示前缀末尾位置(j也表示p[0]~p[i-1]子串的最长公共前后缀的长度)

首先,初始化next,j;

1、初始化:j=0,next[0]=0,

2、处理前后缀不相等情况,当p[i]!=p[j]时,j向前回退,j=next[j-1](j>0),一直回退直到回退到s[i]==s[j]或者j==0

3、处理前后缀相同的情况,当p[i]=p[j]时,j++

4、更新next[i]的值,next[i]=j;

void getNext(vector<int>&next,string str)
{
    int j=0;
    next[0]=0;
    for(int i=1;i<str.size();i++)
    {
        while(j>0&&s[i]!=[j])//处理前后缀不相等的情况
            j=next[j-1];
        if(str[i]==str[j])j++;//处理前后缀相等的情况
        next[i]=j;//更新next
    }
}

5、用上面得到的next数组去与目标串匹配

int strStr(string s,string p)
{
    int n=p.size();
    if(n==0)return 0;
    vector<int>next(n);
    getNext(next,p);
    int j=0;
    for(int i=0;i<s.size();i++)
    {
        while(j>0&&s[i]!=p[j]){//不匹配时,j向后回退
            j=next[j-1];
        }
        if(s[i]==p[j])//字符匹配成功时,j和i一起向右移动,i在for循环内移动
        {
            j++;
        }
        //如果匹配串的最后一个字符匹配成功,则返回目标串中第一次出现匹配串的位置
        if(j==p.size())
        {
            return (i-j+1)
        }
    }
    return -1;
}

KMP算法为什么比朴素算法好用,是因为其根据已经匹配过的再度匹配

leetcode 459. 重复的子字符串

给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。

示例 1:输入: "abab"输出: True解释: 可由子字符串 "ab" 重复两次构成。

示例 2:输入: "aba"输出: False

示例 3:输入: "abcabcabcabc"输出: True解释: 可由子字符串 "abc" 重复四次构成。 (或者子字符串 "abcabc" 重复两次构成。)

思路:首先构建字符串的next数组,计算字符串的长度,当字符串长度减去next[len-1]的差值能被字符串的长度整除,则此字符串就能由他的一个子串重复多次构成,即len%(len-next[len-1])==0

class Solution {
public:
    void getNext(vector<int>&next,string str)
    {
        int j=0;
        next[0]=0;
        for(int i=1;i<str.size();i++)
        {
            while(j>0&&str[i]!=str[j])
            {
                j=next[j-1];
            }
            if(str[i]==str[j])j++;
            next[i]=j;
        }
    }
    bool repeatedSubstringPattern(string s) {
        int n=s.size();
        if(n==0)return true;
        vector<int>next(n);
        getNext(next,s);
        if(next[n-1]!=0&&n%(n-next[n-1])==0)return true;
        return false;
    }
};

排序算法

1. 冒泡排序

/*原理:它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。把最小的数浮上来,或者把最大的数据沉下去。需要进行n-1次排序*/
void quickSort(vector<int>& nums) {
    int n = nums.size();
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1; j++) {
            if (nums[j] > nums[j + 1]) {
                swap(nums[j], nums[j + 1]);
            }
        }
    }
}

2、选择排序

//原理:每轮确定第i个下标,在后面找到最小的值与其互换,需要n-1轮即可
void selectSort(vector<int>& nums) {
    int n = nums.size();
    for (int i = 0; i < n-1; i++) {//每次找出最小的值与i下标的值转换
        int minIndex = i;
        for (int j = i + 1; j < n; j++) {
            if (nums[minIndex] > nums[j])minIndex = j;
        }
        if (minIndex != i) {
            swap(nums[minIndex], nums[i]);
        }
    }
}

3、插入排序

/*原理:从第二位数据开始, 当前数(第一趟是第二位数)与前面的数依次比较,如果前面的数大于当前数,则将这个数放在当前数的位置上,当前数的下标-1,直到当前数不大于前面的某一个数为止。直到遍历至最后一位元素。

通俗的讲,就是从第二位开始,更小的值往前插入。前面的数据肯定是插入排序已经排列好的,前面的值小于或等于当前值基准值的位置。*/
void insertSort(vector<int>& a) {
    for (int i = 1; i < a.size(); ++i)//从第二位开始
    {
        int key = a[i];//插入值
        int j = i - 1;
        while (j >= 0 && a[j] > key) {//前面的值大于当前值,则将当前值往前移动一位,并将j减一,比较更前面的元素
            a[j + 1] = a[j];
            --j;
        }
        //前面的值均小于key,将其插入到后面
        a[j + 1] = key;
    }
    for (int i = 0; i < a.size(); i++) {
        cout << a[i] << "-->";
    }
}

4、快速排序

/*原理:通过一趟排序将序列分成左右两部分,其中左半部分的的值均比右半部分的值小,然后再分别对左右部分的记录进行排序,直到整个序列有序。*/
int partition(vector<int>&a,  int low, int high){
    int key = a[low];//获取基准,基准为low,所以一定要从high开始
    while( low < high ){
        while(low < high && a[high] >= key) high--;//右边查找
        a[low] = a[high];
        while(low < high && a[low] <= key) low++;//左边查找
        a[high] = a[low];
    }
    a[low] = key;//基准最后插入
    return low;
}
void quickSort(vector<int>&a, int low, int high){
    if(low >= high) return;
    int keypos = partition(a, low, high);//一次排序,keypos将数据分成左右两边
    quickSort(a, low, keypos-1);//递归
    quickSort(a, keypos+1, high);
}
//快速排序的改进算法,基于随机起点,防止大数据有序造成的超时
int partition(vector<int>&a,  int low, int high){
    //随机化起点,防止大数据有序,造成最坏时间复杂度,如1,2,3,4,5.....,以1作为分界点显然会造成最坏时间复杂度
    int index=rand()%(high-low+1)+low;
    swap(a[low],a[index]);

    int key = a[low];//获取基准,基准为low,所以一定要从high开始
    while( low < high ){
        while(low < high && a[high] >= key) high--;//右边查找
        a[low] = a[high];
        while(low < high && a[low] <= key) low++;//左边查找
        a[high] = a[low];
    }
    a[low] = key;//基准最后插入
    return low;
}
void quickSort(vector<int>&a, int low, int high){
    if(low >= high) return;
    int keypos = partition(a, low, high);//一次排序,keypos将数据分成左右两边
    quickSort(a, low, keypos-1);//递归
    quickSort(a, keypos+1, high);
}

5、归并排序:时间复杂度O(nlogn),空间复杂度O(n),稳定

void merge(int left,int right,int mid,vector<int>&arr)
{
    int len=0;
    vector<int>temp(len+1);//辅助数组
    int i=left,j=mid+1;
    while(i<=mid&&j<=right)
    {
        if(arr[i]<arr[j])temp[len++]=arr[j++];
        else temp[len++]=arr[i++];
    }
    while(i<=mid)temp[len++]=arr[i++];
    while(j<=right)temp[len++]=arr[j++];
    for(int k=left;k<=right;k++)
    {
        arr[left]=temp[k-left];
    }
}
void mergeSort(int left,int right,vector<int>&arr)
{
    if(left>=right)return;
    int mid=left+(right-left)/2;
    //分割
    mergeSort(left,mid,arr);
    mergeSort(mid+1,right,arr);
    //融合
    merge(left,right,mid,arr);
}

6、堆排序:时间复杂度O(nlogn),空间复杂度O(1),不稳定

堆排序步骤:
1、根据初始元素建立大顶堆(或小顶堆)
2、将大顶堆(或小顶堆)的最大元素与最后一个元素交换,最后一个元素剔除出堆
3、重新生成大顶堆(或小顶堆),再交换
4、一直重复2、3操作,直到只有一个元素0
void BuildHeap(vector<int>&arr,int i,int n)
{
    int p=i;//p为父节点下标
    int c=2*i+1;//c为左孩子下标,因为下标从0开始,所以左孩子下标不是2*i,而是2*i+1
    while(c<=n){
        //首先找出左右孩子中最大的
        if(c<n&&arr[c]<arr[c+1]){
            c++;
        }
        //与父节点比较,如果孩子节点大于父节点,交换其值,此时孩子节点的值变小,需要循环操作,以孩子节点为父节点,再进行比较
        if(arr[c]>arr[p])
        {
            swap(arr[p],arr[c]);//交换父子节点的值
            p=c;
        }
        c=2*c+1;
    }
}
void HeapSort(vector<int>&arr)
{
    //从最后一个非叶子节点开始建立大根堆
    //arr.size()/2求得是从1开始的最后一个非叶子节点的排序序号,arr.size()/2-1是在数组中的下标
    for(int i=arr.size()/2-1;i>=0;i--)
    {
        BuildHeap(arr,i,arr.size()-1);
    }
    for(int i=arr.size()-1;i>=0;i--)
    {
        swap(arr[0],arr[i]);//将堆顶元素与最后一个元素交换
        BuildHeap(arr,0,i-1);//排除最后一个元素,从第一个元素开始重新构建堆
    }
}

7、计数排序

计数排序只能用在数据数值范围不大的场景中,如果数值范围 k 比要排序的数据个数 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。
思想:求出数组的数据范围,根据数据范围设置count数组,遍历nums数组,每遍历一个数,则对应的数的下标所表示的值加一,即count[nums[i]-minNum]++;
vector<int>   sort(vector<int>nums){
    int minNum=*min_element(nums.begin(),nums.end());
    
    int maxNum=*max_element(nums.begin(),nums.end());
    
    vector<int>count(maxNum-minNum+1);
    
    for(int i=0;i<nums.size();i++){
        count[nums[i]-minNum]++;
    }

    vector<int>result(nums.size());
    for(int i=0;i<count.size();i++){
        while(count[i]--)result[j++]=i+minNum;
    }
    return result;
}

8、基数排序

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较,是一种稳定排序算法
将数组中的数值,按先后顺序以个位、十位、百位、千位.....排序,最后便可得到有序数组
首先要先求出数组中数值得最大位数,以最大位数来决定排序得次数,即若最大值为5000,则需要排序四次,按个位,十位,百位,千位得顺序进行排序
void sort(vector<int>&nums){
    vector<int>count(10);//因为每一位上的数的取值范围为0-9
    vector<int>temp(nums.size());
    //求出nums数组中最长的位数,设为len
    for(int i=0;i<len;i++){//循环次数为最大值的位数
        int div=pow(10,i);
        for(int j=0;j<nums.size();j++){//统计每个桶中的记录数
            int num=nums[j]/div%10;
            count[num]++;
        }
        for(int m=1;m<count.size();m++){   //将tmp中的位置依次分配给每个桶
            count[m]+=count[m-1];    //可以获得每个桶中最后一个元素在数组中的下标
        }
        for(int n=nums.szie()-1;n>=0;--n){//将所有桶中记录依次收集到temp中
            int num=nums[n]/div%10;
            temp[--count[num]]=nums[n];    //此时的count[num]-1相当于下标
        }
        for(int k=0;k<nums.size();k++){//将这一次的排序结果放入nums数组中
            nums[k]=temp[k];
        }
        for(int k=0;k<=9;k++){//清空计数器为下一次做准备
            count[k]=0;
        }
    }
}    

9、希尔排序

原理:希尔排序是插入排序改良的算法,是插入排序的一种高效率的实现,也叫缩小增量排序。希尔排序步长从大到小调整,所以步长是关键,最终步长为1,做最后的排序。
步长为1的时候肯定能排出符合的序列。
为什么说是高效率?因为直接插入排序的问题:如果在后面来了一个特别小的元素,需要全部移动,那么排序的效率特别低。而希尔排序可以将小元素很快的移动到前面
void shell_insert(vector<int>&a, int d){
    for(int i = d; i < a.size(); i++){
        int j = i - d;//子序列进行插入排序
        int key = a[i];
        while( j >= 0 && a[j] > key){
            a[j+d] = a[j];
            j -= d;
        }
        if(j != i-d)
            a[j+d] = key;
    }
}
void shellSort(vector<int>&a){
    int d = a.size()/2;//步长
    while(d >= 1){
        shell_insert(a, d);
        d /= 2;
    }
}

10、桶排序

原理
桶排序的原理是将数组中的元素分到多个桶内,对每个桶内的元素分别排序,最后将每个桶内的有序元素合并,即可得到排序后的数组。
桶排序需要预先设定桶的大小(即每个桶最多包含的不同元素个数)和桶的个数,将元素按照元素值均匀分布到各个桶内。对于每个元素,根据该元素与最小元素之差以及桶的大小计算该元素应该分到的桶的编号,可以确保编号小的桶内的元素都小于编号大的桶内的元素。当所有元素都分到桶内之后,对于每个桶分别排序,由于每个桶内的元素个数较少,因此每个桶的排序可以使用插入排序实现。当每个桶都排序结束之后,按照桶的编号从小到大的顺序依次取出每个桶内的元素,拼接之后得到的数组即为排序后的数组。
vector<int> sortArray(vector<int>& nums) {
        int minNum=*min_element(nums.begin(),nums.end());
        int maxNum=*max_element(nums.begin(),nums.end());
        int bucketsize=100;
        //计算桶的个数
        int count=(maxNum-minNum)/bucketsize+1;
        vector<vector<int>>bucket(count);
        for(int i=0;i<nums.size();i++){
            int index=(nums[i]-minNum)/bucketsize;
            bucket[index].push_back(nums[i]);
        }
        int k=0;
        for(int i=0;i<count;i++){
            sort(bucket[i].begin(),bucket[i].end());
            for(auto&x:bucket[i]){
                nums[k++]=x;
            }
        }
        return nums;
    }
复杂度分析
时间复杂度:平均情况是 O(n + k),最差情况是 O(n^2) ,其中 n 是数组的长度,k 是桶的个数。平均情况下,每个桶内的元素个数较少,因此时间复杂度主要取决于将元素分到桶内和拼接每个桶的元素的时间,桶排序的时间复杂度是 O(n + k)。最差情况下,多个元素被分到同一个桶内,对于这个桶排序的时间复杂度是 O(n^2), 桶排序的时间复杂度是 O(n^2)
空间复杂度:O(n + k),其中n 是数组的长度,k 是桶的个数。桶排序需要创建 k 个桶存放全部元素,因此桶排序的空间复杂度是 O(n + k)。
稳定性分析
桶排序的排序过程包括将元素分到桶内、对每个桶分别排序和拼接排序后的结果,其中只有对每个桶分别排序会改变元素之间的相对顺序。只要对每个桶分别排序时使用稳定的排序算法,就能确保相等元素之间的相对顺序总是保持不变,此时桶排序是稳定的排序算法。
  • 25
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值