数据结构笔记

重点

一、数据结构的定义

逻辑结构

集合结构除了同属于一个集合之外,没有其他关系

线状结构数据元素之间是一对一的关系

树形结构数据元素之间是一对多的层次关系

图形结构数据元素之间是多对多的关系

存储结构

线性存储结构数据元素存放在连续的存储单元里,其数据间的逻辑关系和物理关系是一致的

链式存储结构数据元素存放在任意的存储单里,存储单元可以是连续的,也可以是不连续的

分析算法的世界复杂度和空间复杂度

在这里插入图片描述

二、线性表

在这里插入图片描述

1、顺序表和链表的优缺点

顺序表

优点:

  1. 顺序表的内存空间连续。
  2. 尾插、尾删效率较高,时间复杂度是O(1)。
  3. 支持随机访问,可以高效的按下标进行操作,时间复杂度是O(1)。

缺点:

  1. 在顺序表中间插入或删除元素时都涉及到元素的移动,效率较低,时间复杂度为O(N)。
  2. 顺序表长度固定,有时需要扩容。

链表

优点:

  1. 链表的内存空间不连续。

  2. 如果知道要处理节点的前一个位置,则进行插入和删除的复杂度为O(1);

  3. 如果不知道要处理节点的前一个位置,则进行插入和删除的复杂度为O(N)。

  4. 头插、头删的效率高,时间复杂度是O(1)。

  5. 没有空间限制,不会溢出,可以存储很多元素。

缺点:
链表不支持随机访问,查找元素效率低,需要遍历节点,时间复杂度是O(n)。

2、考察链表或者双向链表

1.带头还是不带头

在这里插入图片描述

2.循环还是不循环

在这里插入图片描述

3.单向还是双向

在这里插入图片描述

  • 虽然单链表的结构众多,但大部分常用的还是两种结构。无头单向不循环链表和带头双向循环链表

例题

约瑟夫环

将链表各个元素排在一个圆上,从头开始数,数到第k个元素就删掉,直到链表只有一个元素为止。

struct node
{
    int data;
    node *next;
};
node *creat(int n)
{
    node *head, *tail, *p;
    head = new node;
    tail = new node;
    p = new node;
    p -> data = 1;
    p -> next = NULL;
    head = p;
    tail = p;
    for(int i = 2; i <= n; i ++)
    {
        p = new node;
        p -> data = i;
        p -> next = NULL;
        tail -> next = p;
        tail = p;
    }
    tail -> next = head;
    return head;
}
void del(node *head, int n, int m)
{
    int cnt = 0, s = 0;
    node *pre, *p;
    pre = head;
    while(pre -> next != head)
    {
        pre = pre -> next;
    }
    while(cnt < n - 1)
    {
        p = pre -> next;
        s ++;
        if(s == m)
        {
            s = 0;
            cnt ++;
            pre -> next = p -> next;
            cout << p -> data << " ";
            delete(p);
        }
        else pre = p;
    }
    cout << pre -> data << endl;
}

单链表为啥要设置头节点

  • 有了头结点后****,对在第一个元素结点前插入结点和删除第一个结点,其操作与对其它结点的操作统一了。****
  • 头指针具有标识作用,故常用头指针冠以链表的名字。
  • *为了使空链表与非空链表处理一致,我们通常设一个头结点*

三、栈与队列

入出栈都采取先进后出原则。

中缀式转成后缀式

/*
首先输入字符串,然后遍历字符串进行操作
当当前字符为数字时直接输出
当当前字符为*或/时,输出栈中的*或/号,当前字符入栈
当当前字符为+或-时,输出栈中的字符直到字符为(为止,当前字符入栈
当当前字符为(时,入栈
当当前字符为)时,输出栈中字符直到(为止
*/

后缀式求值

设置一个栈,开始时,栈为空,然后从左到右扫描后缀表达式,若遇操作数,则进栈;若遇运算符,则从栈中退出两个元素,先退出的放到运算符的右边,后退出的 放到运算符左边,运算后的结果再进栈,直到后缀表达式扫描完毕

5 -2 + 3 * #
遇到5, -2,就push到栈中,此时栈中有两个元素,为5, -2,遇到+,从栈顶中取出两个元

素,进行运算,5 +(-2) = 3,获得结果之后,将结果push到栈中,现在栈中只剩下了一个元素

3,然后继续输入,3,push到栈中,此时栈中有两个元素,遇到 ‘*’ 号,从栈顶中取出两个元素,

3, 3,进行乘法运算,3 * 3 = 9,然后将 9 push到栈中,栈中只剩下了一个元素,9,然后继续

输入,此时遇到了 ‘#’ 号,结束读入。
括号匹配

当当前字符为(、【、{时,将括号入栈,当当前字符为)、】、}时,查看栈顶元素是否为其对应的括号,同时栈不可以为空

队列

队列是先进先出

用队列解决约瑟夫环问题

将所有元素入队列,

int main()
{
    int n, m;
    cin >> n >> m;
    queue<int>q;
    for(int i = 1; i <= n; i ++)
        q.push(i);
    while(q.size() > 1)
    {
        for(int i = 0; i < m - 1; i ++)
        {
            int now = q.front();
            q.pop();
            q.push(now);
        }
        cout << q.front() << " ";
        q.pop();
    }
    cout << q.front() << endl;
    return 0;
}

四、kmp

模板

string s, t;
int n, m;
int Next[N];
void dp()
{
    int i = 0, j = -1;
    Next[0] = -1;
    while(i < m)
    {
        if(j == -1 || t[i] == t[j])
        {
            i ++;
            j ++;
            Next[i] = j;
        }
        else j = Next[j];
    }
}
int kmp()
{
    int i = 0, j = 0;
    int cnt = 0;
    while(i < n && j < m)
    {
        if(j == -1 || s[i] == t[j])
        {
            i ++;
            j ++;
        }
        else j = Next[j];
        if(j == m)
        {
            cnt ++;
            j = Next[j];
        }
    }
    return cnt;
}

五、广义表知识点

广义表的基础概念

  1. 什么是广义表

    广义表,又称列表,也是一种线性存储结构,既可以存储不可再分的元素,也可以存储广义表,记作:LS = (a1,a2,…,an),其中,LS 代表广义表的名称,an 表示广义表存储的数据,广义表中每个 ai 既可以代表单个元素,也可以代表另一个广义表。

  2. 广义表的原子和子表

    广义表中存储的单个元素称为 “原子”,而存储的广义表称为 “子表”。
    例如 :广义表 LS = {1,{1,2,3}},则此广义表的构成 :广义表 LS 存储了一个原子 1 和子表 {1,2,3}。
    广义表存储数据的一些常用形式:
    A = ():A 表示一个广义表,只不过表是空的。
    B = (e):广义表 B 中只有一个原子 e。
    C = (a,(b,c,d)) :广义表 C 中有两个元素,原子 a 和子表 (b,c,d)。
    D = (A,B,C):广义表 D 中存有 3 个子表,分别是A、B和C。这种表示方式等同于 D = ((),(e),(b,c,d)) 。
    E = (a,E):广义表 E 中有两个元素,原子 a 和它本身。这是一个递归广义表,等同于:E = (a,(a,(a,…)))。

  3. 广义表的表头和表尾

    当广义表不是空表时,称第一个数据(原子或子表)为"表头",剩下的数据构成的新广义表为"表尾"。
    除非广义表为空表,否则广义表一定具有表头和表尾,且广义表的表尾一定是一个广义表。

广义表的存储结构

求广义表长度时,两种不同的存储方式求解也有所不同,如下示意图所示:

在这里插入图片描述

对于图 1a) 来说,只需计算最顶层(红色标注)含有的节点数量,即可求的广义表的长度。同理,对于图 1b) 来说,由于其最顶层(蓝色标注)表示的此广义表,而第二层(红色标注)表示的才是该广义表中包含的数据元素,因此可以通过计算第二层中包含的节点数量,才可求得广义表的长度。

在这里插入图片描述

六、树和二叉树

先序中序后序

根左右,左根右,左右根

在这里插入图片描述

在这里插入图片描述

层序遍历

从上到下,从左到右遍历

void cengxu(node *root)
{
    int in = 0, out = 0;
    node *q[55];
    q[in ++] = root;
    while(in > out)
    {
        if(q[out])
        {
            cout << q[out] -> data;
            q[in ++] = q[out] -> l;
            q[in ++] = q[out] -> r;
        }
        out ++;
    }
}

以上算法都是时间复杂度o(n),空间复杂度o(n)

还原二叉树、创建二叉树

还原二叉树

先序遍历字符串输入,如果为叶子节点输出NULL,否则创造新节点,左建树,右建树

struct node
{
    int data;
    node *l, *r;
};
char a[N];
int k = 0;
node *build()
{
    node *root;
    if(a[k ++] == ',') return NULL;
    root = new node;
    root -> data = a[k];
    root -> l = build();
    root -> r = build();
    return root;
}

创建二叉树

先序中序输出后序

struct node
{
    int data;
    node *l, *r;
};
char pre[N], mid[N];
node *build(int len, char *pre, char *mid)
{
    if(!len) return NULL;
    node *root = NULL;
    root = new node;
    root -> data = pre[0];
    int i;
    for(i = 0; i < len; i ++)
    {
        if(mid[i] == pre[0])
            break;
    }
    root -> l = build(i, pre + 1, mid);
    root -> r = build(len - i - 1, pre + i + 1, mid + 1 + i);
    return root;
}
void postorder(node *root)
{
    if(root)
    {
        postorder(root -> l);
        postorder(root -> r);
        cout << char(root -> data);
    }
}

中序和后序输出先序

struct node
{
    int data;
    node *l, *r;
};
char mid[N], post[N];
node *build(int len, char *mid, char *post)
{
    if(!len) return NULL;
    node *root = NULL;
    root = new node;
    root -> data = post[len - 1];
    int i;
    for(i = 0; i < len; i ++)
    {
        if(mid[i] == post[len - 1])
            break;
    }
    root -> l = build(i, mid, post);
    root -> r = build(len - 1 - i, mid + i + 1, post + i);
    return root;
}
void preorder(node *root)
{
    if(root)
    {
        cout << char(root -> data);
        preorder(root -> l);
        preorder(root -> r);
    }
}

二叉树的六个性质

性质1:二叉树第i层上的结点数目最多为2^(i-1)(i>=1)

性质2:深度为i的二叉树至多有2 ^(i)-1个结点,至少有2 ^(i-1)个结点(i>=1)

性质3:包含n个结点的二叉树的高度至少为
( l o g 2 ​ n ) + 1 (log2​ n)+1 (log2​n)+1
性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1

证明:
n为总结点数,n1为度为1的结点总数,n0,n2同理;
由(1)(2)

n=n0+n1+n2 (1)
n=n0 *0+n1 *1+n2 2+1即 n=n1+2n2+1 (2)

得:n0=n2+1

性质5:具有n个结点的完全二叉树的深度为
f l o o r ( l o g 2 n ) ( 向下取整 ) + 1 floor(log2n)(向下取整)+1 floor(log2n)(向下取整)+1
性质6:将一颗完全二叉树依次编号1-n;
结点编号间关系:

                                            floor(i/2)
                                                |
                                                i
                                              /   \  
                                          2i      2i+1

求深度求叶子

求深度

int deep(node *root)
{
    int d1, d2;
    if(root)
    {
        d1 = deep(root -> l);
        d2 = deep(root -> r);
        return max(d1, d2) + 1;
    }
    return 0;
}

求叶子

出度为0的为叶子

树与二叉树,树二叉树和森林之间的转化

  1. 将树转换为二叉树:树中每个结点最多只有一个最左边的孩子(长子)和一个右邻的兄弟。按照这种关系很自然地就能将树转换成相应的二叉树:1.在所有兄弟结点之间加一连线2.对每个结点,除了保留与其长子的连线外,去掉该结点与其它孩子的连线。如下图所示:

    在这里插入图片描述

  2. 将一个森林转换为二叉树:

    具体方法是:1.将森林中的每棵树变为二叉树;2.因为转换所得的二叉树的根结点的右子树均为空,故可将各二叉树的根结点视为兄弟从左至右连在一起,就形成了一棵二叉树。

    如下图所示:

    在这里插入图片描述

  3. 二叉树转换为树:

    是树转换为二叉树的逆过程。

    1.加线。若某结点X的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点…,都作为结点X的孩子。将结点X与这些右孩子结点用线连接起来。

    2.去线。删除原二叉树中所有结点与其右孩子结点的连线。

    如下图所示:

    在这里插入图片描述

  4. 二叉树转换为森林:

    假如一棵二叉树的根节点有右孩子,则这棵二叉树能够转换为森林,否则将转换为一棵树。

    1.从根节点开始,若右孩子存在,则把与右孩子结点的连线删除。再查看分离后的二叉树,若其根节点的右孩子存在,则连线删除…。直到所有这些根节点与右孩子的连线都删除为止。

    2.将每棵分离后的二叉树转换为树。

    如下图所示:

    在这里插入图片描述

七、图论

基本算法:图的邻接矩阵存储,图的邻接表存储,图的正向反向转化,会画邻接矩阵邻接表,有向图会画逆邻接表,还原图,bfs(分层,dp,最短路)和dfs(图的连通集)的算法

最小生成树的prime算法和克鲁斯卡尔算法

最短路的迪杰斯特拉算法,floyd, 拓扑排序算法,欧拉回路

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

邻接矩阵和邻接表存储知识点

邻接矩阵就是一个二维数组存储信息

邻接表就是vector<vector>mp;

邻接矩阵适用于稠密图(边数接近于顶点数的平方),邻接表适用于稀疏图(边数远小于顶点数的平方)。

bfs

存储

BFS是一种借用队列来存储的过程,分层查找,优先考虑距离出发点近的点。无论是在邻接表还是邻接矩阵中存储,都需要借助一个辅助队列,v个顶点均需入队,最坏的情况下,空间复杂度为O(v)。

邻接表形式存储时,每个顶点均需搜索一次,时间复杂度T1=O(v),从一个顶点开始搜索时,开始搜索,访问未被访问过的节点。最坏的情况下,每个顶点至少访问一次,每条边至少访问1次,这是因为在搜索的过程中,若某结点向下搜索时,其子结点都访问过了,这时候就会回退,故时间复 杂度为O(E),算法总的时间复 度为O(|V|+|E|)。

邻接矩阵存储方式时,查找每个顶点的邻接点所需时间为O(V),即该节点所在的该行该列。又有n个顶点,故算总的时间复杂度为O(|V|^2)。

算法实现

将起点放到队列里,从队列头节点开始遍历,将与他连着的点放到队列里,然后继续遍历队列头节点,重复之前的,把与他连着的点放到队列里,最后遍历完成,全部节点入队

void bfs(int op)
{
	queue<int>q;
	q.push(op);
	vis[op] = 1;
	while(q.size())
	{
		int now = q.front();
		q.pop();
		cout << now << " ";
		for(int i = 0; i < mp[now].size(); i ++)
		{
			if(!vis[mp[now][i]])
			{
				vis[mp[now][i]] = 1;
				q.push(mp[now][i]);
			}
		}
	}
}

dfs

存储

DFS算法是一一个递归算法,需要借助一个递归工作栈,故它的空问复杂度为O(V)。

遍历图的过程实质上是对每个顶点查找其邻接点的过程,其耗费的时间取决于所采用结构。

邻接表表示时,查找所有顶点的邻接点所需时间为O(E),访问顶点的邻接点所花时间为O(V),此时,总的时间复杂度为O(V+E)。

邻接矩阵表示时,查找每个顶点的邻接点所需时间为O(V),要查找整个矩阵,故总的时间度为O(V^2)。

v为图的顶点数,E为边数。

算法实现

将起点放到队列里,然后遍历队列,取出头节点,搜索与他连着的点,然后搜索与他连着的点的连着的点,然后是连着的点的连着的点的连着的点。。。。。

vector<int>mp[N];
int vis[N];
int n, m;
void dfs(int op)
{
    if(op > n) return;
    cout << op << " ";
    vis[op] = 1;
    for(int i = 0; i < mp[op].size(); i ++)
    {
        if(!vis[mp[op][i]])
        {
            dfs(mp[op][i]);
        }
    }
}
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i ++)
    {
        int u, v;
        cin >> u >> v;
        mp[u].push_back(v);
    }
    for(int i = 0; i < n; i ++)
        sort(mp[i].begin(), mp[i].end());
    for(int i = 0; i < n; i ++)
    {
        if(!vis[i])
        {
            dfs(i);
        }
    }
    return 0;
}

最小生成树

克鲁苏卡尔

将所有边排序,从小到大依次加边,加边过程中不可以成环,直到加完n-1条边

struct node
{
    int u, v, w;
}a[N];
int f[N];
bool cmp(node a, node b)
{
    return a.w < b.w;
}
int Find(int x)
{
    return x == f[x] ? x : f[x] = Find(f[x]);
}
int Merge(int x, int y)
{
    int a = Find(x);
    int b = Find(y);
    if(a != b)
    {
        f[b] = a;
        return 1;
    }
    return 0;
}
int main()
{
    int n, m;
    cin >> n >> m;
    for(int i = 0; i <= n; i ++)
        f[i] = i;
    for(int i = 1; i <= m; i ++)
    {
        int u, v, w;
        cin >> u >> v >> w;
        a[i].u = u;
        a[i].v = v;
        a[i].w = w;
    }
    sort(a + 1, a + 1 + m, cmp);
    int cnt = 0, ans = 0;
    for(int i = 1; i <= m; i ++)
    {
        int x = a[i].u, y = a[i].v;
        if(Merge(x, y))
        {
            cnt ++;
            ans += a[i].w;
        }
    }
    if(cnt != n - 1) puts("-1");
    else cout << ans << endl;
    return 0;
}

普利姆算法

从起点开始遍历,将这个点看作一个集合,然后找离这个集合最近的一个点,然后将这个点放入集合,然后再重复操作。

int mp[N][N];
int vis[N];
int dis[N];
int n, m;
int prime()
{
    mem(dis, INF);
    dis[1] = 0;
    int ans = 0;
    for(int i = 1; i <= n; i ++)
    {
        int u = -1, Min = INF;
        for(int j = 1; j <= n; j ++)
        {
            if(!vis[j] && dis[j] < Min)
            {
                u = j;
                Min = dis[j];
            }
        }
        if(u == -1) return -1;
        vis[u] = 1;
        ans += dis[u];
        for(int j = 1; j <= n; j ++)
        {
            if(!vis[j] && mp[u][j] != INF && dis[j] > mp[u][j])
            {
                dis[j] =  mp[u][j];
            }
        }
    }
    return ans;
}
int main()
{
    mem(mp, INF);
    cin >> n >> m;
    while(m --)
    {
        int u, v, w;
        cin >> u >> v >> w;
        mp[u][v] = w;
        mp[v][u] = w;
    }
    cout << prime();
    return 0;
}

最短路

迪杰斯特拉

朴素版迪杰斯特拉 O(n^2)

vector<PII>mp[20010];
int dis[20010], vis[20010];
int n, m;
void dijkstra()
{
    mem(dis, 0x3f);
    dis[0] = 0;
    for(int i = 1; i <= n; i ++)
    {
        int u = -1, Min = 999999;
        for(int j = 0; j < n; j ++)
        {
            if(!vis[j] && Min > dis[j])
            {
                u = j;
                Min = dis[j];
            }
        }
        if(u == -1) break;
        vis[u] = 1;
        for(int j = 0; j < mp[u].size(); j ++)
        {
            int v = mp[u][j].xx;
            int w = mp[u][j].yy;
            if(!vis[v])
            {
                dis[v] = min(dis[v], dis[u] + w);
            }
        }
    }
}
int main()
{
    cin >> n >> m;
    while(m --)
    {
        int u, v, w;
        cin >> u >> v >> w;
        mp[u].push_back({v, w});
    }
    dijkstra();
    for(int i = 1; i < n; i ++)
    {
        if(dis[i] != INF)
        {
            cout << dis[i] << " ";
        }
    }
    return 0;
}

floyd

	for(int k = 1; k <= n; k ++)
    {
        for(int i = 1; i <= n; i ++)
        {
            for(int j = 1; j <= n; j ++)
            {
                f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
            }
        }
    }

拓扑排序

将所有点统计入度,入度为0的点为根节点,将根节点入队列,然后取出头节点,删去头节点,将头节点连着的每一个点入度都减一,当减到0时入队,直到元素遍历完,入队顺序就是拓扑顺序。

时间复杂度:O(n + e)

空间复杂度:O(n)

void toop()
{
    queue<int>q;
    for(int i = 0; i < n; i ++)
    {
        if(in[i] == 0)
            q.push(i);
    }
    int cnt = 0, ans = 0;
    while(q.size())
    {
        int now = q.front();
        q.pop();
        cnt ++;
        for(int i = 0; i < n; i ++)
        {
            if(mp[now][i] != -1)
            {
                in[i] --;
                if(in[i] == 0)
                    q.push(i);
                dis[i] = max(dis[now] + mp[now][i], dis[i]);
                ans = max(dis[i], ans);
            }
        }
    }
    if(cnt != n) puts("Impossible");
    else cout << ans << endl;
}

欧拉回路

度全都是偶数即为欧拉图

八、查找表

静态:折半;动态:二叉排序树

二分查找

//分成[l, mid][mid + 1, r]找min
int erfen()
{
    int l = 0, r = INF;
    while(l < r)
    {
        int mid = l + r >> 1;
        if(check(mid))
            r = mid;
        else l = mid + 1;
    }
    return l;
}
// 分成[l, mid - 1][mid, r], 找max
int erfen()
{
    int l = 0, r = INF;
    while(l < r)
    {
        int mid = l + r + 1 >> 1;
        if(check(mid))
            l = mid;
        else r = mid - 1;
    }
    return l;
}

二叉排序树

左边的儿子一定比根节点小,右边的儿子大于等于根节点

查找

//查找的递归算法
BSTNode *Search(BSTNode *root, int x)
{
    if(root->data == x)
    {
        return root;
    }
    else if(x < root->data)
    {
        return Search(root->left, x);
    }
    else
    {
        return Search(root->right, x);
    }
}

插入

//插入的递归算法
BSTNode *Insert(BSTNode *root, int x){
    if(root == NULL){
        root = CreateTreeNode(x);
        return root;
    }
    if(x < root->data){
        root->left = Insert(root->left, x);
    }
    if(x > root->data){
        root->right = Insert(root->right, x);
    }
    return root;
}

删除

  1. 删叶子节点:直接删
  2. 删有一个子树的:把子树连到要删除的节点的根节点上
  3. 删有两个节点的:找到右子树的最小值,然后用1、2操作删除它,然后安到我们要删的节点上

优秀博客:如何从二叉搜索树中删除节点? (baidu.com)

平均查找长度

  1. 查找成功的平均查找长度

    在这里插入图片描述

  2. 查找不成功的平均查找长度

    在这里插入图片描述

九、哈希表

哈希的存储,线性探测,平方探测,拉链探测;查找成功的平均长度,查找不成功的平均长度,ALS

参考( 哈希.docx)

当哈希表后边没表格的时候,再从头开始找

平均查找长度

精彩博客:哈希表平均查找长度_数据结构哈希表查找长度_好饿呀~~~的博客-CSDN博客

十、排序

基本思想:冒泡,插入,希尔,快排,堆排,归并排序,桶排,基数排序 的思想以及应用,优先级的比较,复杂度的比较,总体性能的比较,对序列初始状态的要求,稳定性的问题。

1.基本流程

快排

i指针放l,j指针放r,j–,直到比al小的时候停止,i++,直到比al大的时候停止,然后互换,最后互换ai和al,让al搁中间,然后再递归排序两边的序列

void q_sort(int l, int r)
{
    int i = l, j = r;
    if(l > r) return;
    while(i < j)
    {
        while(a[j] >= a[l] && i < j)
            j --;
        while(a[i] <= a[l] && i < j)
            i ++;
        if(i < j) swap(a[i], a[j]);
    }
    swap(a[i], a[l]);
    q_sort(l, i - 1);
    q_sort(i + 1, r);
}

归并排序

分成单个区间,然后合并的时候两两排序,直到排好

int tmp[N], a[N], n;
//这里为需要背诵的部分,要求:给一个相关题目,能快速写出该模板并调试通过。
//归并排序
void merge_sort(int q[], int l, int r)
{
    //递归出口
    if (l >= r) return;
    
    //第一步,分成两个子区间
    int mid =  l + r >> 1;
    
    //第二部,递归处理子区间
    //要注意这里用的mid和mid+1来划分两区间,建议不要用mid-1和mid来划分
    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r);
    
    //第三步,合并排序好的子区间
    //tips:k为tmp下标,i和j为两个子区间起始位置
    int k = 0, i = l, j = mid + 1;
    //排序好的两边取大小,暂存数组tmp挑小的取
    while (i <= mid && j <= r) 
    {
        if (q[i] <= q[j]) tmp[k++] = q[i++];
        else tmp[k++] = q[j++];
    }
    //很可能存在有一子区间没有比较完,由于该区间是排好序的,后面的没比较完,说明都是最大的,直接往tmp后面加即可。
    while (i <= mid) tmp[k++] = q[i++];
    while (j <= r) tmp[k++] = q[j++];
    //在[l, r]范围中,将tmp数组存的有序值赋给q数组完排序,注意<=r的等号不要漏
    for (int i = l, j =0; i <= r; i++, j++) q[i] = tmp[j];
}

堆排序

1.首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端

2.将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1

3.将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组

注意:升序用大根堆,降序就用小根堆(默认为升序)

int cnt;
int h[N];
void down(int x)
{
    int t = x;
    if (x * 2 <= cnt && h[x * 2] > h[t])
        t = x * 2;
    if (x * 2 + 1 <= cnt && h[x * 2 + 1] > h[t])
        t = x * 2 + 1;
    if (x != t)
    {
        swap(h[x], h[t]);
        down(t);
    }
}
void heap_sort(int l, int r)
{
    cnt = 0;
    for (int i = l; i <= r; i++)
    {
        h[++cnt] = a[i];
    }
    for (int i = cnt / 2; i; i--)
    {
        down(i);
    }
    int k = r;
    while (cnt)
    {
        a[k --] = h[1];
        h[1] = h[cnt--];
        down(1);
    }
}

计数排序

计算ai的数量,然后算它在数组中是第几小的,然后把第几小赋给b数组作为下标,同时b下标=a中的值,最后整理下

int a[N], h[N+50];
int b[N];
int n;//如果出现负数 (-10000,10000)整体右移 x+10000 (0,20000)
//如果小数 整体*100
void counting_sort() 
{
    int w = 100050;
    memset(h, 0, sizeof h);
    for (int i = 0; i < n; i++) h[a[i]]++;
    for (int i = 1; i <= w; i++) h[i] += h[i - 1];
    for (int i = n-1; i >= 0; i--) b[h[a[i]]--] = a[i];
    for(int i=1;i<=n;i++)
    a[i-1]=b[i];
}

桶排序

桶排序的核心思想就是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶排序完之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

int n, w = 100000, a[N];
vector<int> bucket[N];

void insert_sort(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) 
        {
            A[j + 1] = A[j];
            j--;
        }
        A[j + 1] = key;
    }
}
void bucket_sort() 
{
    int bucket_size = w / n + 1;
    for (int i = 0; i < n; i++) 
    {
        bucket[i].clear();
    }
    for (int i = 0; i < n; i++) 
    {
        bucket[a[i] / bucket_size].push_back(a[i]);
    }
    int p = 0;
    for (int i = 0; i < n; i++) 
    {
        insert_sort(bucket[i]);
        for (int j = 0; j < bucket[i].size(); j++) 
        {
            a[p++] = bucket[i][j];
        }
    }
}
// I prefer it
vector<int> bucket[N];
int w = 100000;
void bucket_sort()
{
    int bucket_size = w/n + 1;
    for(int i = 0; i < n; i ++)
        bucket[i].clear();
    for(int i = 0; i < n; i ++)
    {
        bucket[a[i]/bucket_size].push_back(a[i]);
    }
    int p = 0;
    for(int i = 0; i < n; i ++)
    {
        sort(bucket[i].begin(), bucket[i].end());
        for(auto it : bucket[i])
            a[p ++] = it;
    }
}

基数排序

const int N = 100010;
const int W = 100010;
const int K = 100;
int n, w[K], k, cnt[W];
struct Element {
    int key[K];
    bool operator<(const Element& y) const {
        for (int i = 1; i <= k; ++i) {
            if (key[i] == y.key[i]) continue;
            return key[i] < y.key[i];
        }
        return false;
    }
} a[N], b[N];
void counting_sort(int p) {
    memset(cnt, 0, sizeof(cnt));
    for (int i = 1; i <= n; ++i) ++cnt[a[i].key[p]];
    for (int i = 1; i <= w[p]; ++i) cnt[i] += cnt[i - 1];
    for (int i = n; i >= 1; --i) 
    b[cnt[a[i].key[p]]--] = a[i];
    memcpy(a, b, sizeof(a));
}
void radix_sort() {
    for (int i = k; i >= 1; --i) {
        counting_sort(i);
    }
}

2.各种算法的特点

在这里插入图片描述

堆排序快速排序希尔排序直接选择排序是不稳定的排序算法,而冒泡排序直接插入排序折半插入排序归并排序是稳定的排序算法。

  • 18
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值