重点
一、数据结构的定义
逻辑结构
集合结构:除了同属于一个集合之外,没有其他关系
线状结构:数据元素之间是一对一的关系
树形结构:数据元素之间是一对多的层次关系
图形结构:数据元素之间是多对多的关系
存储结构
线性存储结构:数据元素存放在连续的存储单元里,其数据间的逻辑关系和物理关系是一致的
链式存储结构:数据元素存放在任意的存储单里,存储单元可以是连续的,也可以是不连续的
分析算法的世界复杂度和空间复杂度
二、线性表
1、顺序表和链表的优缺点
顺序表:
优点:
- 顺序表的内存空间连续。
- 尾插、尾删效率较高,时间复杂度是O(1)。
- 支持随机访问,可以高效的按下标进行操作,时间复杂度是O(1)。
缺点:
- 在顺序表中间插入或删除元素时都涉及到元素的移动,效率较低,时间复杂度为O(N)。
- 顺序表长度固定,有时需要扩容。
链表 :
优点:
-
链表的内存空间不连续。
-
如果知道要处理节点的前一个位置,则进行插入和删除的复杂度为O(1);
-
如果不知道要处理节点的前一个位置,则进行插入和删除的复杂度为O(N)。
-
头插、头删的效率高,时间复杂度是O(1)。
-
没有空间限制,不会溢出,可以存储很多元素。
缺点:
链表不支持随机访问,查找元素效率低,需要遍历节点,时间复杂度是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;
}
五、广义表知识点
广义表的基础概念
-
什么是广义表
广义表,又称列表,也是一种线性存储结构,既可以存储不可再分的元素,也可以存储广义表,记作:LS = (a1,a2,…,an),其中,LS 代表广义表的名称,an 表示广义表存储的数据,广义表中每个 ai 既可以代表单个元素,也可以代表另一个广义表。
-
广义表的原子和子表
广义表中存储的单个元素称为 “原子”,而存储的广义表称为 “子表”。
例如 :广义表 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,…)))。 -
广义表的表头和表尾
当广义表不是空表时,称第一个数据(原子或子表)为"表头",剩下的数据构成的新广义表为"表尾"。
除非广义表为空表,否则广义表一定具有表头和表尾,且广义表的表尾一定是一个广义表。
广义表的存储结构
求广义表长度时,两种不同的存储方式求解也有所不同,如下示意图所示:
对于图 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
(log2n)+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.在所有兄弟结点之间加一连线2.对每个结点,除了保留与其长子的连线外,去掉该结点与其它孩子的连线。如下图所示:
-
将一个森林转换为二叉树:
具体方法是:1.将森林中的每棵树变为二叉树;2.因为转换所得的二叉树的根结点的右子树均为空,故可将各二叉树的根结点视为兄弟从左至右连在一起,就形成了一棵二叉树。
如下图所示:
-
二叉树转换为树:
是树转换为二叉树的逆过程。
1.加线。若某结点X的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点…,都作为结点X的孩子。将结点X与这些右孩子结点用线连接起来。
2.去线。删除原二叉树中所有结点与其右孩子结点的连线。
如下图所示:
-
二叉树转换为森林:
假如一棵二叉树的根节点有右孩子,则这棵二叉树能够转换为森林,否则将转换为一棵树。
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操作删除它,然后安到我们要删的节点上
优秀博客:如何从二叉搜索树中删除节点? (baidu.com)
平均查找长度
-
查找成功的平均查找长度
-
查找不成功的平均查找长度
九、哈希表
哈希的存储,线性探测,平方探测,拉链探测;查找成功的平均长度,查找不成功的平均长度,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.各种算法的特点
堆排序、快速排序、希尔排序、直接选择排序是不稳定的排序算法,而冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。