序言:
该文章以主要算法及其代码块、个人掌握度较低的内容为主
部分内容参照了西工大2024年头歌题目
可能无法包含书中所有算法
如需更详细的复习内容可以参考其他文章
行文风格:无序号,以块为主要划分
省去了串的内容,本人掌握较好且不重要故没有写
请重点关注树及其以后的内容
有些代码的注释较少,结合严版教材看或许可以理解得更快一些
零、算法分析
评价算法质量
正确性、可读性、健壮性、效率与低存储要求
算法效率的度量
语句的频度
一条语句出现的次数
(正常算就行了)
时间复杂度
对数 < 线性 < 线性对数 < 多项式 < 指数 对数\lt 线性\lt 线性对数\lt 多项式\lt 指数 对数<线性<线性对数<多项式<指数
1 < l o g 2 n < n < n l o g 2 n < n k < 2 n < n ! 1\lt log_2n\lt n\lt nlog_2n\lt n^k\lt 2^n \lt n! 1<log2n<n<nlog2n<nk<2n<n!
具体计算方法请见时间复杂度计算方法
算法的存储空间需求
空间复杂度
输入数据空间+程序本身空间+前两者之外的辅助空间
一、线性表
链表归并问题
首先是合并,双指针法即可解决
齐次是元素大小顺序的问题,用头插法或者尾插法即可解决
注意:不要忘了一个归并完了另一个链表可能还没有归并完
void Sort (list *head1, list *head2, list *temhead)
{
node *m = head1;
node *n = head2;
while (m -> next && n -> next){
if (m -> next -> data >= n -> next -> data) {
node *q = (node*)malloc(sizeof(node));
q -> next = temhead -> next;
temhead -> next = q;
q -> data = n -> next -> data;
n = n -> next;
}
else {
node *q = (node*)malloc(sizeof(node));
q -> next = temhead -> next;
temhead -> next = q;
q -> data = m -> next -> data;
m = m -> next;
}
}
while (m -> next){
node *q = (node*)malloc(sizeof(node));
q -> next = temhead -> next;
temhead -> next = q;
q -> data = m -> next -> data;
m = m -> next;
}
while (n -> next){
node *q = (node*)malloc(sizeof(node));
q -> next = temhead -> next;
temhead -> next = q;
q -> data = n -> next -> data;
n = n -> next;
}
}
删除重复元素
A链表中删除B、C中均有的元素(删除问题)
双指针法
node *r = head -> next;
while (i < n && j < p){
if (arr1[i] < arr2[j])
i++;
else if (arr1[i] > arr2[j])
j++;
else{
while (r -> next){
if (r -> next -> data == arr1[i]){
node *tem = r -> next;
r -> next = tem -> next;
free(tem);
}
else if(r -> next -> data < arr1[i])
r = r -> next;
else
break;
}
i++;j++;
}
}
Locate操作
使用双链表,便于查找
冒泡排序法,不断向后依次判断,大的向前挪动
//双链表创建方法
list *InitLinkList (int size)
{
list *head = (list*)malloc(sizeof(list));
head->next = head->prior = head;
head->data = ' ';
head->freq = 0;//访问频度
node *tem = head;
for (int i = 0; i < size;){
char a = getchar();
if (a != ' ' && a != '\n'){
node *p = (node*)malloc(sizeof(node));
p->prior = tem->prior;
p->next = tem;
tem->prior->next = p;
tem->prior = p;
p->data = a;
p->freq = 0;
i++;
}
}
return head;
}
//Locate方法
void Sort (list *head)
{
node *p = head->next->next;
for (; p != head;){
node *bigger = p->prior;
node *next = p->next;
while (bigger != head && p->freq > bigger->freq){
bigger = bigger->prior;
}
p->prior->next = p->next;
p->next->prior = p->prior;
//改变p前后的元素的节点
p->prior = bigger;
p->next = bigger->next;
//改变p节点
bigger->next->prior = p;
bigger->next = p;
//改变p节点周围的节点
p = next;
//下一步比较
}
}
二、栈和队列
栈:先进后出
队列:先进先出
双向栈
#define stack_size 100
#define stackincrement 10
typedef struct stack{
int* base[2];
int* front[2];
int size;
}stack;
void Init_stack (stack tws)
{
tws.base[0] = (int *)malloc(stack_size * sizeof(stack));
tws.base[1] = tws.base[0] + stack_size - 1;
tws.front[0] = tws.base[0];
tws.front[1] = tws.base[1];
tws.size = stack_size;
}
循环队列
#include <stdbool.h>
//循环队列
int k; //队列长度
typedef struct Queen{
int base[1000];
int rear;
int front;
int flag;
int size;//队列中实际存储数据长度
}queen;
queen Q;
Q.size = (k + Q.rear - Q.front) % k;
void InitQueen(queen Q)//初始化
{
Q.front = Q.rear = 0;
Q.flag = 0;
}
bool Push (queen Q, int x)//入队列
{
if (Q.flag)
return 0;
Q.base[Q.rear] = x;
Q.rear = (Q.rear + 1) % k;
if (Q.rear == Q.front)
Q.flag = 1;
return 1;
}
bool Pop(queen Q, int* x)//出队列
{
if (!Q.flag)
return 0;
*x = Q.base[Q.front];
Q.front = (Q.front + 1) % k;
if (Q.rear == Q.front)
Q.flag = 0;
return 1;
}
应用
应用1:求逆波兰式
重点理解优先级的判断函数
分为多种情况:
(1)直接输出:字母
(2)直接入栈:起始符号+优先级高的符号
(3)直接出栈:遇到优先级小的直到目前符号优先级变高了或者栈空+遇到右括号直到遇到左括号
#include <stdio.h>
#include <stdbool.h>
int top = -1;
char stack[500] = " ";
char str[500] = " ";
//符号优先级比较
bool isprior (char curr, char top)
{
if ( (((curr == '*') || (curr == '/')) && ((top == '+') || (top == '-'))) || (top == '(') || (curr == '(') )
return true;
else
return false;
}
//判断字母
bool isword (char s)
{
if (s - 'a' >= 0 && 'z' - s >= 0)
return true;
else
return false;
}
//逆向
void rePoland ()
{
fgets(str, 500, stdin);
char *ch = str;
while (*ch){
//判断结束
if (*ch == '\n')
break;
//是字母
if (isword(*ch))
printf ("%c", *ch);
//不是字母——运算符号入栈
else if (top == -1 || isprior(*ch, stack[top]))
stack[++top] = *ch;
//不是字母——下一符号是右括号——运算符号出栈——直到遇到左括号
else if (*ch == ')'){
while (stack[top] != '(')
printf ("%c", stack[top--]);
top--;//左括号出栈
}
//不是字母——优先级小——运算符出栈——直到优先级大或者栈空
else {
while (top != -1 && !isprior(*ch, stack[top]))
printf ("%c", stack[top--]);
stack[++top] = *ch;
}
ch++;
}
while (top != -1)
printf ("%c", stack[top--]);
}
int main()
{
rePoland();
return 0;
}
应用2:括号表达式括号匹配
注意获取表达式的方式
原则:紧邻相消,直至栈空,否则为0
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
char str[500] = " ";
char stack[500] = " ";
int top = 0;
bool match ()
{
fgets (str, 500, stdin);
for(char *p = str; *p; p++){
switch(*p){
case '(':
case '[':
case '{':{
stack[top++] = *p;
break;
}
case ')':{
if (stack[--top] != '(')
return false;
break;
}
case ']':{
if (stack[--top] != '[')
return false;
break;
}
case '}':{
if (stack[--top] != '{')
return false;
break;
}
default: break;
}
}
if (top == 0)
return true;
return false;
}
int main()
{
if (match())
printf ("yes");
else
printf ("no");
return 0;
}
应用3:汉诺塔问题
递归算法和栈的结合
int c = 0;
void move (char x, int n, char z)
{
printf ("第%d次:将%d从%c移动到%c上去", ++c, n, x, z);
}
void hanoi (int n, char x, char y, char z)
//n为盘子数量,xyz为三个柱子
{
if (n == 1)
move(x, 1, z);
else{
hanoi(n-1, x, z, y);
move(x, n, z);
hanoi(n-1, y, x, z);
}
}
应用4:k阶斐波那契数列
其中使用了循环数列
#include <stdio.h>
int main()
{
int max, k;
scanf ("%d %d", &max, &k);
int Queue[500] = {0}, rear = 0, sum = 0;
Queue [k - 1] = 1;
for (;1;rear = (rear + 1) % k){
sum = 0;
for (int i = 0; i < k; i++){
sum += Queue[i];
}
if (Queue[rear] <= max && sum > max)
break;
Queue[rear] = sum;
}
for (int front = 0;front < k; front++){
int i = (front + rear) % k;
printf ("%d ", Queue[i]);
}
printf ("\n");
return 0;
}
三、数组和广义表
数组的存储地址计算方式
L O C ( I , J ) = L O C ( 0 , 0 ) + L O C ( S i z e o f E l e m e n t × I + J ) LOC(I,J) = LOC(0,0) + LOC(Size of Element \times I + J) LOC(I,J)=LOC(0,0)+LOC(SizeofElement×I+J)
那么由此引申为基址+编址
二维数组原则:行优先原则
矩阵压缩——在稀疏矩阵(说白了就是0太多了浪费了大量的空间)中常用——存储结构:三元表、二元表
三元表
typedef struct {
int col;
int raw;
int data;
}element;
typedef struct {
element arr[1000];//往往只需要这个就够了
int numcol;//列数
int numraw;//行数
int num;//非零元个数
};
三元表实现矩阵相加
原则:先看行后看列,不相等则赋值,若相等则相加
void Add (array arr1[], array arr2[], array arr[], int t1, int t2, int *flag)
{
int i = 0, j = 0, m = 0;
for (; i < t1 && j < t2;){
if (arr1[i].raw < arr2[j].raw){//arr1行小于arr2行,则arr1直接赋值
arr[m].raw = arr1[i].raw;
arr[m].col = arr1[i].col;
arr[m++].data = arr1[i++].data;
(*flag) ++;
}
else if (arr1[i].raw > arr2[j].raw){//arr2行小于arr1行,则arr2直接赋值
arr[m].raw = arr2[j].raw;
arr[m].col = arr2[j].col;
arr[m++].data = arr2[j++].data;
(*flag) ++;
}
else {//arr1行与arr2行相等
if (arr1[i].col < arr2[j].col){//直接赋值列小的
arr[m].raw = arr1[i].raw;
arr[m].col = arr1[i].col;
arr[m++].data = arr1[i++].data;
(*flag) ++;
}
else if (arr1[i].col > arr2[j].col) {//直接赋值列小的
arr[m].raw = arr2[j].raw;
arr[m].col = arr2[j].col;
arr[m++].data = arr2[j++].data;
(*flag) ++;
}
else {//行列均相等则直接相加
arr[m].raw = arr2[j].raw;
arr[m].col = arr2[j].col;
arr[m++].data = arr2[j++].data + arr1[i++].data;
(*flag) ++;
}
}
}
}//加法
三元表实现矩阵相乘
原则:按照正常的矩阵乘法法则计算即可
void MatrixMultiplication(array arr1[], array arr2[], array arr3[], int raw1, int col2, int cnt1, int cnt2, int *cnt)
{//四指针法
int sum = 0, k = 0;
for (int temraw1 = 1; temraw1 <= raw1; temraw1++){//arr3的行
for (int temcol2 = 1; temcol2 <= col2; temcol2++){//arr3的列
for (int i = 0; i < cnt1; i++){
for (int j = 0; j < cnt2; j++){//找arr1的列与arr2的行相同的元素
if (arr1[i].raw == temraw1 && arr2[j].col == temcol2 && arr1[i].col == arr2[j].raw)//保证了结果矩阵在赋值的时候
// 也能按照顺序赋值,不需要人工去排序
sum += arr1[i].data * arr2[j].data;//矩阵相乘运算
}
}
if (sum != 0){//给arr3赋值,如果最后结果是零则不需要赋值
arr3[k].raw = temraw1;
arr3[k].col = temcol2;
arr3[k++].data = sum;
sum = 0;
(*cnt)++;//记录结果矩阵非零元素的个数,方便输出
}
}
}
}//矩阵相乘
二元表(为了转置而诞生)
二元表转置
注意行列对调
void Transpose (int num[], int cpot[], int raw, int col, array arr1[], array arr2[], int cnt)
{
for (int i = 1; i <= cnt; i++){
num[arr1[i].col]++;//计算arr1中每一列非零元素的个数
}
cpot[1] = 1;//第一个元素的位置为1
for (int i = 2; i <= col; i++){
cpot[i] = cpot[i - 1] + num[i - 1];
}//计算arr1中每一列不同元素在arr2中的不同起始位置
for (int i = 1; i <= cnt; i++){
int tem1 = arr1[i].col;
int tem2 = cpot[tem1];
arr2[tem2].raw = arr1[i].col;
arr2[tem2].col = arr1[i].raw;
arr2[tem2].data = arr1[i].data;
cpot[tem1]++;
}//进行转置
}//转置
十字链表
原则:边建立边输入
例如:十字链表实现矩阵相加
//初始化:一个头节点+两串导航用的头节点数组
list *Initlinklist (int raw, int col)
{
list *head = (list*)malloc(sizeof(list));
head->raw = raw;
head->col = col;
head->down = head;
head->right = head;
head->data = -1;
node *tem = head;
for (int i = 1; i <= raw; i++){
node *p = (node*)malloc(sizeof(node));
p->data = -1;
p->col = 0;
p->raw = i;
p->right = p;
tem->down = p;
p->down = head;
tem = p;
}
tem = head;
for (int i = 1; i <= col; i++){
node *p = (node*)malloc(sizeof(node));
p->data = -1;
p->col = i;
p->raw = 0;
p->down = p;
tem->right = p;
p->right = head;
tem = p;
}
return head;
}
//关注插入新节点部分,先用桥找出行,再将实际节点关联到桥上,再用桥找列,再将实际节点关联到列上即可;删除节点更为简单,不再写代码赘述
void Add (list *head, int raw, int col, int data)
{
node *tem = head;
for (int i = 0; i < raw; i++)
tem = tem->down;
while (tem->right->col < col && tem->right->col != 0)
tem = tem->right;
if (tem->right->col == col){
tem->right->data += data;
return ;
}
node *p = (node*)malloc(sizeof(node));
p->right = tem->right;
tem->right = p;
tem = head;
for (int i = 0; i < col; i++)
tem = tem->right;
while (tem->down->raw < raw && tem->down->raw != 0)
tem = tem->down;
p->down = tem->down;
tem->down = p;
p->data = data;
p->raw = raw;
p->col = col;
}
广义表
一个循环嵌套结构
不过多赘述,理论题会做就行
四、树和二叉树
二叉树(本身有序)
1、二叉树的性质:
① 第 i 层上至多有 2 i − 1 个节点,其中根节点是第一层 第i层上至多有2^{i-1}个节点,其中根节点是第一层 第i层上至多有2i−1个节点,其中根节点是第一层
② 深度为 k 的二叉树至多有 2 k − 1 个节点 深度为k的二叉树至多有2^k-1个节点 深度为k的二叉树至多有2k−1个节点
③ 对任何一棵二叉树 T ,如果其叶子个数为 n 0 ,度为 2 的结点数为 对任何一棵二叉树T,如果其叶子个数为n_0,度为2的结点数为 对任何一棵二叉树T,如果其叶子个数为n0,度为2的结点数为
n 2 ,则 n 0 = n 2 + 1 n_2,则n_0=n_2+1 n2,则n0=n2+1
(在数据结构中,度为离散数学中结点的出度) (在数据结构中,度为离散数学中结点的出度) (在数据结构中,度为离散数学中结点的出度)
(这个结论的典例是呈三角形排列的只有三个结点的二叉树) (这个结论的典例是呈三角形排列的只有三个结点的二叉树) (这个结论的典例是呈三角形排列的只有三个结点的二叉树)
④(外加两个另外的定义:满二叉树:顾名思义就是满的;完全二叉树:在满二叉树中倒序依次砍掉若干个叶子结点即可)
具有 n 个节点的完全二叉树的深度为 ⌊ l o g 2 n ⌋ + 1 或 ⌈ l o g 2 ( n + 1 ) ⌉ 具有n个节点的完全二叉树的深度为\lfloor log_2n\rfloor + 1或\lceil log_2(n+1)\rceil 具有n个节点的完全二叉树的深度为⌊log2n⌋+1或⌈log2(n+1)⌉
⑤ 如果对一棵有 n 个结点的完全二叉树的结点按层序编号 如果对一棵有n个结点的完全二叉树的结点按层序编号 如果对一棵有n个结点的完全二叉树的结点按层序编号
(从第 1 层到第 ⌊ l o g 2 n ⌋ + 1 层,每层从左到右) (从第1层到第\lfloor log_2n\rfloor + 1层,每层从左到右) (从第1层到第⌊log2n⌋+1层,每层从左到右)
则对任一合法结点 i ,有 则对任一合法结点i,有 则对任一合法结点i,有
1 )若 i = 1 ,则结点 i 是二叉树的根; 1)若i=1,则结点i是二叉树的根; 1)若i=1,则结点i是二叉树的根;
i > 1 ,那么双亲结点是 ⌊ i 2 ⌋ 。 i\gt 1,那么双亲结点是\lfloor \frac{i}{2} \rfloor。 i>1,那么双亲结点是⌊2i⌋。
2 )若 2 i > n ,则结点 i 无左孩子,否则左孩子结点为 2 i 2)若2i\gt n,则结点i无左孩子,否则左孩子结点为2i 2)若2i>n,则结点i无左孩子,否则左孩子结点为2i
3 )若 2 i + 1 > n ,则结点无右孩子,否则右孩子结点为 2 i + 1 3)若2i+1\gt n,则结点无右孩子,否则右孩子结点为2i+1 3)若2i+1>n,则结点无右孩子,否则右孩子结点为2i+1
遍历二叉树(两种结构)
递归法遍历二叉树
//递归法先序遍历
void PreOrderTraverse(BTree tree)
{
if (tree){
printf ("%d", tree.data);
PreOrderTraverse(tree->lchild);
PreOrderTraverse(tree->rchild);
}
}
//中序
void InOrderTraverse(BTree)
{
if (tree){
InOrderTraverse(tree.lchild);
printf ("%d", tree.data);
InOrderTraverse(tree,rchild);
}
}
//后序
void PostOrderTraverse(BTree tree)
{
if (tree){
PostOrderTraverse(tree->lchild);
PostOrderTraverse(tree->rchild);
printf ("%d", tree.data);
}
}
栈遍历二叉树
//声明:Pop(p,S)函数不仅栈指针下移动,还将旧栈顶元素赋值给了p
//栈先序遍历
bool PreOrderTraverse(BTree T)
{
InitStack(S);//初始化栈
BTree p = T;//临时变量
while (p || !StackEmpty(S)){//条件是防止到了最左下角的元素无法进入循环
if (p){
Push(p, S)//当前入栈
p = p.lchild;//往左找
}
else{
Pop(p, S);//无左孩子时,退栈
if(!visit(p.data))
return ERROR;
visit(p.data);//输出当前结点
p = p.rchild;//找上一个结点的右孩子
}
}
return OK;
}
//中序
bool InOrderTraverse(BTree T)
{
InitStack(S);
BTree p = T;
while (p || !StackEmpty(S)){
if (p){
Push(p, S)
visit(p);
p = p.lchild;
}
else{
Pop(p, S);
p = p.rchild;
}
}
}
//后序
void PreOrderTraverse(BTree T)
{
InitStack(S);
BTree p = T;
while (p || !StackEmpty(S)){
if (p){
Push(p, S)
p = p.lchild;
}
else{
Pop(p, S);
if (!p.rchild || p.rchild == tem){
visit(p);
tem = p;//时刻记录上一个访问的结点
p = NULL;//把p置空,防止再次向p的左孩子遍历
}
else {
p = p.rchild;
}
}
}
return OK;
}
层序遍历二叉树
就是广搜算法,用栈就行了,不再赘述
由前序、中序求二叉链表
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct node{
struct node *l, *r;
char data;
}Node, Root;
char prearr[500] = "";
char midarr[500] = "";
Root *CreateTree(int prel, int prer, int midl, int midr)
{
if (prel > prer || midl > midr)
return NULL;
Node *p = (Node*)malloc(sizeof (Node));
p->l = p->r = NULL;
p->data = prearr[prel];
int cnt = 0;
while (midarr[cnt] != prearr[prel])
cnt++;
int llen = cnt - midl;
p->l = CreateTree(prel + 1, prel + llen, midl, cnt - 1);
p->r = CreateTree(prel + llen + 1, prer, cnt + 1, midr);
return p;
}
void PostTranverse(Node *p)
{
if (!p)
return;
PostTranverse(p->l);
PostTranverse(p->r);
printf ("%c", p->data);
}
int main()
{
scanf ("%s", prearr);
scanf ("%s", midarr);
int lenpre = strlen(prearr);
int lenmid = strlen(midarr);
Root *root = CreateTree(0, lenpre - 1, 0, lenmid - 1);
PostTranverse(root);
return 0;
}
建立二叉树
可以采用递归遍历二叉树的方法进行条件建立,说白了就是在递归遍历二叉树的基础上加点条件判断语句就行了
#include <stdio.h>
#include <stdlib.h>
typedef struct node{
struct node *l, *r;
char data;
}Node, Root;
Root *createtree ()
{
char a = getchar();
Node *p = (Node*)malloc(sizeof(Node));
p->l = NULL;
p->r = NULL;
p->data = a;
a = getchar();
if (a == '(') {
p->l = createtree();
p->r = createtree();
}
else if (a == ',') {
p->l = NULL;
p->r = NULL;
}
else if (a == ')') {
p->l = NULL;
p->r = NULL;
a = getchar();
}
return p;
}
void Tranverse(Node *p)
{
if(!p)
return;
printf ("%c", p->data);
Tranverse(p->l);
Tranverse(p->r);
}
int main()
{
Root *root = createtree();
Tranverse(root);
return 0;
}
线索二叉树
中序线索二叉树
//前期准备,头结点的构造
void InOrderThreading (Head h, BTree p)
{
h = (Head*)malloc(sizeof(BTreenode));
h.lfalg = 0;
h.rflag = 1;
h.rchild = h;
if (!p)
h.lchild = h;
else {
h.lchild = p;
BTreenode *pre = h;
Threading(p);
pre.rchild = h;
pre.rflag = 1;
h.rchild = pre;
}
}
//中序线索二叉树,0是指针,1是线索
void InThreading (BTree p)
{
if (p){
InThreading(p.lchild);
if (!p.lchild){
p.lflag = 0;
p.lchild = pre;
}
else
p.lchild = 1;
if (!pre.rchild){
pre.rflag = 0;
pre.rchild = p;
}
else
pre.rflag = 1;
pre = p;
InThreading(p.rchild);
}
}
树的存储结构
1、双亲表示法:顺序表中至记录元素的双亲的序号
2、孩子表示法:顺序表+单链表,单链表中记录的是该结点的所有孩子
3、孩子兄弟表示法:用二叉树来记录,每一斜右下的一层为兄弟,左下一层为孩子
树、森林与而二叉树的转换
(本人离散数学中有提及)
1、树转化成二叉树:弟弟变右儿子
2、森林转化成二叉树:树内部采用1、的办法,树之间则采用将剩下的每棵二元树作为左边的二元树的根的右子树即可
哈夫曼编码
哈夫曼树
重点理解在编码的时候如何利用编码数组的内存:从数组的最末尾进行使用,但是最末尾并不是数组大小的最末尾,而是叶子结点个数的最末尾
//本程序从0开始使用,而非1
//本程序使用结构体数组构建哈夫曼树、哈夫曼编码、译码表
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
//构造哈夫曼树
typedef struct HTree{
char data;
int weight;
int parent, left, right;
}HTree;
//构造记录哈夫曼编码的结构
typedef struct HC{
char data;
int begin;
int code[500];
}HC;
//构造记录哈夫曼译码的结构
typedef struct Code{
int co[500];
int num;
}Code;
Code code;
//构造哈夫曼树
HTree *createHTree(int num)
{
int n = 2 * num;
HTree *htree = (HTree*)calloc(n, sizeof(HTree));//申请哈夫曼树内存
int min1, min2, lc, rc;//定义最小、最大节点,左、右孩子节点
for (int i = 0; i < num;){//输入哈夫曼树节点值
htree[i].parent = 0;
htree[i].left = htree[i].right = 0;
char ch = getchar();
if (ch != ' ' && ch != '\n') htree[i++].data = ch;
}
for (int i = 0; i < num; i++)
scanf ("%d", &htree[i].weight);//输入哈夫曼树节点权值
for (int i = num; i < n - 1; i++){//找到最小和次小的节点
min1 = INT_MAX, min2 = INT_MAX, lc = 0, rc = 0;
for (int j = 0; j <= i - 1; j++){//遍历已存在的节点
if (htree[j].weight < min1 && htree[j].parent == 0){//找到最小的节点
min2 = min1;
min1 = htree[j].weight;
rc = lc;
lc = j;
}
else if (htree[j].weight < min2 && htree[j].parent == 0){//找到次小的节点
min2 = htree[j].weight;
rc = j;
}
}
htree[i].weight = htree[lc].weight + htree[rc].weight;//合起来之后更新权值
htree[i].left = lc;//更新新节点的左孩子
htree[i].right = rc;//更新新节点的右孩子
htree[lc].parent = htree[rc].parent = i;//更新节点的父节点
}
htree[n - 2].parent = 0;//将根节点的父节点设为0
return htree;
}
HC *createHC(HTree *htree, int num)//构造哈夫曼编码表
{
HC *hc = (HC*)calloc(num + 1, sizeof(HC));//申请空间
for (int i = 0; i < num; i++){//构建哈夫曼编码
int fa = htree[i].parent;
int curr = i;
hc[i].begin = num;//倒着遍历所以要倒着输入
hc[i].data = htree[i].data;//字母输入进去
for (;fa;){
if (htree[fa].left == curr)
hc[i].code[hc[i].begin] = 0;//左则0
else
hc[i].code[hc[i].begin] = 1;//右则1
curr = fa;//逐层往父节点走
fa = htree[curr].parent;//父节点逐层往父节点走
hc[i].begin--;//输入位置前走一位
}
}
return hc;
}
void encode(HC *hc, int num)//输出哈夫曼编码的二进制码
{
char arr[500] = "";
scanf ("%s", arr);//输入报文
int len = strlen(arr);//得到报文长度
int tem = 0;
for (int i = 0; i < len; i++){//遍历报文
for (int j = 0; j < num; j++){//遍历字母
if (arr[i] == hc[j].data){//判断报文和字母是否匹配
for (int k = hc[j].begin + 1; k <= num; k++){//若匹配,则从前往后(循环)输出报文对应的二进制码
printf ("%d", hc[j].code[k]);
code.co[tem++] = hc[j].code[k];//记录输出的二进制码到code.co数组中
code.num++;//记录二进制码的长度
}
}
}
}
printf ("\n");
}
void decode (HTree *htree, Code code, int num)//输出二进制码对应的报文
{
int tem;
int i = 2 * num - 2;//找到根节点
for (int j = 0; j < code.num; j++){
if (code.co[j] == 0){//如果是0则向左查找
tem = htree[i].left;
i = tem;//更新位置向左查找
if (htree[tem].left == 0 && htree[tem].right == 0){//无孩子则找到叶子节点,可以输出
printf ("%c", htree[tem].data);
i = 2 * num - 2;//退回到根节点
}
}
else if (code.co[j] == 1){//如果是1则向右查找
tem = htree[i].right;
i = tem;//更新位置向右查找
if (htree[tem].left == 0 && htree[tem].right == 0){//无孩子则找到叶子节点,可以输出
printf ("%c", htree[tem].data);
i = 2 * num - 2;//退回到根节点
}
}
}
printf ("\n");
}
//主函数
int main()
{
int num;
code.num = 0;
scanf ("%d", &num);
HTree *htree = createHTree(num);//构造哈夫曼树
HC *hc = createHC(htree, num);//构造哈夫曼编码表
encode(hc, num);//输出哈夫曼编码的二进制码
decode(htree, code, num);//输出二进制码对应的译码
return 0;
}
五、图
注意区分弧头和弧尾:弧头是终止结点,弧尾是起始结点
图的存储结构:
1、数组表示法
2、邻接表表示法:顺序表+单向链表,类似于树种的孩子表示法,把其能够连通的所有结点都当作孩子链接到后面
3、十字链表:麻烦至极……
图的遍历
基于图的DFS深度优先算法
使用递归算法
用邻接表存储时时间复杂度: O ( V + E ) O(V+E) O(V+E)
用临界矩阵存储时间复杂度: O ( V 2 ) O(V^2) O(V2)
空间复杂度: O ( V ) O(V) O(V)
void DFS(Graph *head, int v1, int v2)
{
Graph *tem = &head[v1];
while (tem){
if (tem->data == v2)
access = 1;
if (!isvisited[tem->data]){
isvisited[tem->data] = 1;
DFS(head, tem->data, v2);
}
tem = tem->next;
}
}
基于图的BFS广度优先算法
使用队列
用邻接表存储时时间复杂度: O ( V + E ) O(V+E) O(V+E)
用临界矩阵存储时间复杂度: O ( V 2 ) O(V^2) O(V2)
空间复杂度: O ( V ) O(V) O(V)
void BFS(Graph *head, int v1, int v2)
{
Queue *queue = (Queue*)malloc(sizeof (Queue));
queue->data = -1;
queue->next = NULL;
Node *front = (Node*)malloc(sizeof (Node));
queue->next = front;
front->data = v1;
front->next = NULL;
Node *rear = front;
while(front){
Graph *p = &head[front->data];
while (p){
if (p->data == v2){
access = 1;
return;
}
if (!isVisited[p->data]){
isVisited[p->data] = 1;
Node *node = (Node*)malloc(sizeof (Node));
rear->next = node;
node->data = p->data;
rear = node;
}
p = p->next;
}
queue->next = front->next;
front = front->next;
}
}
最小生成树
Prim算法
针对结点的算法,划分两个点集,找这两个点集间最短的桥即可
时间复杂度: O ( n 2 ) O(n^2) O(n2)
typedef struct Closest{
int v;
int lowcost;
}closest;
closest edge[g.num];//g,num为图中结点的个数
void Prim(Graph g, int u)//u为起始结点
{
int k = Locate(g, u);//找到u在图中的位置,说白了就是u的value
for (int i = 0; i < g.num; i++){
if (i != k){
edge[i].v = k;
edge[i].lowcost = g.length[k][i];
}
}//初始化辅助数组
edge[k].v = 0;
edge[k].lowcost = 0;//初始化辅助数组
for (int i = 0; i < g.num; i++){
for (int j = 0; j < g.num && edge[k].lowcost != 0; j++){
k = INT_MAX;
if (edge[j].lowcost < k)
k = j;
}
printf (edge[k].v -> k);
edge[k].lowcost = 0;
for (int j = 0; j < g.num && edge[j].lowcost != 0; j++){
if (g.length[k][j] < edge[j].lowcost){
edge[j].lowcost = g.length[k][j];
edge[j].v = k;
}
}
}
}
Kruskal克鲁斯卡尔算法
针对边的算法,按权值从小到大寻找边,但是避免形成回路
了解即可
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
有向无环图及拓补排序
拓补排序
先随便找一个没有前驱的结点;
然后输出并在图中删除这个结点;
然后重复上述两步直至所有结点都输出;
如果没能输出所有结点,说明该图中存在自锁
//拓补排序
//前提设置:采用邻接矩阵记录有向图的信息
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
int graph[500][500];
bool isvisited[500] = {};
void TopologicalSorting(int v)
{
for (int i = 0; i < v; i++)
isvisited[i] = 0;//初始化访问数组
int num = 0;//记录是否将所有结点都访问完毕
while (1){
int i = 0;
for (; i < v; i++){//循环遍历图中的结点
int tem = 0;
for (int j = 0; j < v && isvisited[i] == 0; j++){//找到没有前驱的结点
if (graph[j][i] == 0)
tem++;
else
break;
}
if (tem == v && isvisited[i] == 0){
for (int j = 0; j < v; j++)
graph[i][j] = 0;//将关联的结点的前驱删除
printf ("%d ", i);//输出当前结点
isvisited[i] = 1;
break;
}
}
num++;
if (num == v)
break;
}
}
void Creategraph(int v)
{
for (int i = 0; i < v; i++)
for (int j = 0; j < v; j++)
scanf ("%d", &graph[i][j]);
}
int main()
{
int v;
scanf ("%d", &v);
Creategraph(v);
TopologicalSorting(v);
return 0;
}
寻找最短路径及其长度
注意路径长度和路径的区别:
在求最短路径长度时,一般直接存储在辅助数组或者图数组之中,直接调用即可
在求最短路径时,一般存储在中,需要现用现求
DIJ迪杰斯特拉算法
应用场景:适用于某一个顶点到其余所有顶点的最短路径
使用数组来记录图
初始化辅助数组以划分两个点集——寻找两点集间最短的边——更新辅助数组
最后所有的最短路径都存储在辅助数组之中
时间复杂度: O ( V 2 ) O(V^2) O(V2)
用距离数组存储的空间复杂度: O ( V ) O(V) O(V)
应用一:0号结点到其余所有结点的最短路径长度
//0号结点到其余所有结点的最短路径
#define MAX 10000
int graph[105][105] = {};
int length[105] = {};
bool isvisited[105] = {};
void Dijkstra(int v)
{
for (int i = 0; i < v; i++)
length[i] = graph[0][i];
isvisited[0] = true;
for (int i = 0; i < v; i++){
int min = INT_MAX;
int vn = i + 1;
for (int j = 0; j < v; j ++){
if (!isvisited[j] && length[j] < min){
min = length[j];
vn = j;
}
}
isvisited[vn] = true;
for (int j = 0; j < v; j++){
if (!isvisited[j] && length[vn] + graph[vn][j] < length[j] && graph[vn][j] != MAX)
length[j] = length[vn] + graph[vn][j];
}
}
}
应用二:求任意两个结点间的最短路径(存放在了栈stack之中)
从终结点倒着找,如果0到i的长度+i到当前结点的长度=0到当前结点的最短长度,那么i就是我们要找的中继结点。注意定好一个起始结点(例如代码中的0结点)是关键!
思考:那么如何求任意一个结点到其余所有结点的最短路径:将任意两个结点间的最短路路径的算法外加一个for循环,也就是将终结点改成其余所有结点即可
#include <stdio.h>
#include <stdbool.h>
#include <limits.h>
#define MAX 10000
int graph[105][105] = {};
int length[105] = {};
bool isvisited[105] = {};
int stack[105] = {};
int ceiling = -1;
void CreateGraph(int v)
{
for (int i = 0; i < v;i++)
for (int j = 0; j < v; j++)
scanf ("%d", &graph[i][j]);
}
void Dijkstra(int v)
{
for (int i = 0; i < v; i++)
length[i] = graph[0][i];
isvisited[0] = true;
for (int i = 0; i < v; i++){
int min = INT_MAX;
int vn = i + 1;
for (int j = 0; j < v; j ++){
if (!isvisited[j] && length[j] < min){
min = length[j];
vn = j;
}
}
isvisited[vn] = true;
for (int j = 0; j < v; j++){
if (!isvisited[j] && length[vn] + graph[vn][j] < length[j] && graph[vn][j] != MAX)
length[j] = length[vn] + graph[vn][j];
}
}
}
void Tranverse(int v, int v1, int v2)
{
stack[++ceiling] = v2;
while (v1 != v2){
for (int i = 0; i < v; i++){
if (length[i] + graph[i][v2] == length[v2] && graph[i][v2] != MAX){
v2 = i;
stack[++ceiling] = i;
}
}
}
for (; ceiling > -1;)
printf ("%d\n", stack[ceiling--]);
}
int main()
{
int v, v1, v2;
scanf ("%d", &v);
CreateGraph(v);
Dijkstra(v);
scanf ("%d %d", &v1, &v2);
Tranverse(v, v1, v2);
return 0;
}
FLD弗洛伊德算法
应用场景:使用于寻找每一对结点间的最短长度
使用数组来记录图
可以在原有的数组中更改数据,不需要辅助数组
始终在任意两个结点间找是否存在一个结点使当路径长度小于当前找到的路径长度——更新最短路径长度
时间复杂度: O ( n 3 ) O(n^3) O(n3)
空间复杂度: O ( n 2 ) O(n^2) O(n2)
应用一:求任意两结点之间最短的路径长度
存储在图数组中
void Floyd(int v)
{
for (int i = 0; i < v; i++)
for (int m = 0; m < v; m++)
for (int n = 0; n < v; n++)
if (graph[m][i] + graph[i][n] < graph[m][n])
graph[m][n] = graph[m][i] + graph[i][n];
}
应用二:求任意两结点之间最短路径
采用递归搜索法
在初始化图时,注意要把自回路设置成特殊标志,方便结束递归(见代码第四行条件判断)
typedef struct G{
int length;
int path;
}Graph;
Graph graph[105][105];
int stack[105] = {0};
int ceiling;
void CreateGraph(int v)
{
for (int i = 0; i < v; i++)
for (int j = 0; j < v; j++){
scanf ("%d", &graph[i][j].length);
graph[i][j].path = -1;//将直接连接的两点记录为-1
}
}
void Floyd(int v)
{
for (int i = 0; i < v; i++)
for (int j = 0; j < v; j++)
for (int k = 0; k < v; k++){
if (graph[j][i].length + graph[i][k].length < graph[j][k].length){
graph[j][k].length = graph[j][i].length + graph[i][k].length;
graph[j][k].path = i;//非直接连接的两点记录为中间搭桥结点
}
}
}
void Search(int v1, int v2)
{
stack[++ceiling] = v2;
if (graph[v1][v2].path == -1){//若直接相连那么入栈
stack[++ceiling] = v1;
return;
}
else
Search(v1, graph[v1][v2].path);//非直接相连则回退到桥上继续寻找
}
void Tranverse(int v)
{
int n;
scanf ("%d", &n);
for (int k = 0; k < n; k++){
int v1, v2;
ceiling = -1;
scanf ("%d %d", &v1, &v2);
Search(v1, v2);
for (; ceiling > -1;){
printf ("%d\n", stack[ceiling--]);
}
}
}
六、查找
静态查找
顺序表查找
按照一定顺序比较值与目标值是否相等
可以设置哨兵,判断是否查找完,不用每一次都判断是否越界(当然这是在不知道表长的情况下)
平均查找长度为 3 4 ( n + 1 ) \frac{3}{4}(n+1) 43(n+1)
时间复杂度: O ( n ) O(n) O(n)
有序表查找——折半查找
就是二分法
平均查找长度: l o g 2 ( n + 1 ) + 1 log_2(n + 1) + 1 log2(n+1)+1
时间复杂度: O ( l o g 2 n ) O(log_2n) O(log2n)
int BSearch(int arr[], int p)
{
int length = strlen(arr);
int low = 0;
int high = length - 1;
while (low <= high){
int min = (low + high) / 2;
if (arr[mid] = p)
return mid;//表示查找成功
else if (arr[mid] < p)
low = mid + 1;
else
high = mid - 1;
}
return -1;//表示查找失败
}
有序表查找——斐波那契查找
按照斐波那契数列队表进分割即可
时间复杂度: O ( l o g n ) O(logn) O(logn)
索引顺序表查找
类似于折半查找,只不过分成的块不是两块而已,而是多块,块间有序,块内无序
数据总共需要两部分:索引表(由最大关键字和起始地址构成)和表,
查找时对索引表进行折半查找,对表进行顺序查找
时间复杂度: O ( l o g n ) O(logn) O(logn)
typedef struct IndexTable{
int maxkey[100];//分割的块中最大的关键字
int locatekey[100];//改块中最大关键字在整个表中的起始地址
int length;
}IT;
int IndexSearch (IT table,int arr[], int p)
{
int low = 0, high = table.length - 1;
int flag;
while (low <= high){
int mid = (low + high) / 2;
if (table.maxkey[mid] < p)
low = mid + 1;
else if (table.maxkey[mid] > p && high - low > 1)
high = mid - 1;
else {
flag = mid;
break;
}
}
for (int i = table.locatekey[flag]; i < table.locatekey[flag + 1];i++){
if (table.maxkey[i] == p)
return i;//查找成功
}
return -1;//查找失败
}
动态查找
二叉排序树
左子树的所有结点的值小于根节点的值,右子树的所有结点的值大于根节点的值
详情见二叉排序树C语言实现(超级详细代码)
二叉排序树的中序遍历是原数组从小到大的排序
时间复杂度: O ( l o g n ) O(logn) O(logn)
平均查找长度: P ( n ) ≤ 2 ( 1 + 1 n ) P(n)\le 2(1+\frac{1}{n}) P(n)≤2(1+n1)ln n n n
平衡二叉树(在二叉排序树的基础上实现)
只有四种情况:左左型、右右型、左右型、右左型
时间复杂度: O ( l o g n ) O(logn) O(logn)
平均查找长度: O ( l o g n ) O(logn) O(logn)
#include<stdio.h>
#include<stdlib.h>
#define max(a,b) ((a>b)?(a):(b))
typedef char DataType;
typedef struct AVLNode
{
DataType data; // 数据域
struct AVLNode *lchild; // 指针域,指向左孩子结点
struct AVLNode *rchild; // 指针域,指向右孩子结点
int height; // 表示当前结点作为根结点子树的高度
}AVLNode, *AVLTree;
// 初始化
void InitAVLTree(AVLTree *T)
{
(*T) = NULL; // 初始化根节点为空
printf("二叉树已初始化!\n");
}
// 获取子树高度
int getHeight(AVLNode *node)
{
return node == NULL ? 0: node->height;
}
// 右旋转
AVLNode* Rotate_R(AVLNode *node)
{
AVLNode *p = node->lchild;
node->lchild = p->rchild;
p->rchild = node;
// 更新高度(注意顺序)
node->height = max(getHeight(node->lchild), getHeight(node->rchild)) + 1;
p->height = max(getHeight(p->lchild), getHeight(p->rchild)) + 1;
return p;
}
// 左旋转
AVLNode* Rotate_L(AVLNode *node)
{
AVLNode *p = node->rchild;
node->rchild = p->lchild;
p->lchild = node;
// 更新高度(注意顺序)
node->height = max(getHeight(node->lchild), getHeight(node->rchild)) + 1;
p->height = max(getHeight(p->lchild), getHeight(p->rchild)) + 1;
return p;
}
// 先左旋转,再右旋转
AVLNode* Rotate_LR(AVLNode *node)
{
AVLNode *p = node->lchild;
node->lchild = Rotate_L(p);
return Rotate_R(node);
}
// 先右旋转,再左旋转
AVLNode* Rotate_RL(AVLNode *node)
{
AVLNode *p = node->rchild;
node->rchild = Rotate_R(p);
return Rotate_L(node);
}
// 计算平衡因子
int getBalance(AVLNode *node)
{
if (node == NULL)
{
return 0;
}
return getHeight(node->lchild) - getHeight(node->rchild);
}
// 排序二叉树的插入操作
void AVLTreeInsert(AVLTree *T, DataType x)
{
if ((*T) == NULL)
{
// 如果当前二叉树结点为空, 即是插入的位置,则分配新的结点空间
(*T) = (AVLNode *)malloc(sizeof(AVLNode));
(*T)->data = x;
(*T)->height = 1;
(*T)->lchild = (*T)->rchild = NULL;
}
else if (x < (*T)->data)
{
// 递归左子树查找插入位置
AVLTreeInsert(&(*T)->lchild, x);
}
else if(x > (*T)->data)
{
// 递归右子树查找插入位置
AVLTreeInsert(&(*T)->rchild, x);
}
else
{
printf("插入失败!存在相同数据!\n");
return;
}
// 在递归插入过程中更新父结点的高度
(*T)->height = max(getHeight((*T)->lchild), getHeight((*T)->rchild)) + 1;
// 获取平衡因子
int balance = getBalance((*T));
printf("结点:%c, 高度 = %d, 平衡因子 = %d\t",(*T)->data, (*T)->height, balance);
// 根据平衡因子选择 旋转方式
if (balance > 1 && x < (*T)->lchild->data)
{
printf("右旋");
(*T) = Rotate_R((*T));
}
else if (balance < -1 && x > (*T)->rchild->data)
{
printf("左旋");
(*T) = Rotate_L((*T));
}
else if (balance > 1 && x > (*T)->lchild->data)
{
printf("左右旋");
(*T) = Rotate_LR((*T));
}
else if (balance < -1 && x < (*T)->rchild->data)
{
printf("右左旋");
(*T) = Rotate_RL((*T));
}
printf("\n");
}
// 先序遍历
void PreOrderTraverse(AVLTree T)
{
if (T)
{
printf("%c ", T->data);
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
}
}
int main()
{
AVLTree T;
InitAVLTree(&T);
DataType x;
while (1)
{
printf("输入插入数据:");
fflush(stdin);
scanf("%c", &x);
if (x == '#')
{
break;
}
AVLTreeInsert(&T, x);
printf("先序遍历:");
PreOrderTraverse(T);
printf("\n");
}
system("pause");
return 0;
}
//计算平衡因子
int Compbf1 (BSTNode * &b)
{
int max1 , max2;
if (b-=NULL)
return 0;
if(b->lchild==NULL && b->rchild==NULL)
{
b->bf=0;
printf(" %d: 8d\n", b->key,b->bf);
return 1;
}
else
{
max1=Compbf1 (b->lchild);
max2-Compbf1 (b->rchild);
b->bf-max1-max2;
printf(" %d: %d\n",b->key,b->bf);
return (max1>max2 ?max1+1 :max2+1);
}
}
B树
性质
结点构成:结点元素个数域+指针域+元素域
n阶B树的结点中元素个数不得超过n-1
每个结点至多有n棵子树
根节点至少有2棵子树
除根节点外的非叶子结点至少有 ⌈ m / 2 ⌉ \lceil m/2\rceil ⌈m/2⌉棵子树
所有叶子必须出现在同一层
结点元素大小与二叉排序树近似,自行理解
B树的查找
与二叉平衡树近似,自行理解
B树的插入
始终在叶子处插入
①若插入后依旧满足性质(即元素个数不多于m-1)那么不用管了
②若插入后元素大于m-1,那么结点分裂,将中间的元素上升到双亲结点之中,两边的留下来分裂后当作两个结点(因为双亲结点每多一个结点就会多产生一个指针域)(结合书中P242的图个更好理解)
B树的删除
先查找后删除
①若所在结点的元素个数不少于 ⌈ m / 2 ⌉ \lceil m/2\rceil ⌈m/2⌉,直接删除就好了
②若所在结点的元素个数等于 ⌈ m / 2 ⌉ − 1 \lceil m/2\rceil - 1 ⌈m/2⌉−1,所在结点的所有紧邻兄弟结点的元素个数大于 ⌈ m / 2 ⌉ − 1 \lceil m/2\rceil - 1 ⌈m/2⌉−1 //以上为条件
(为了方便理解我加了断句符)
将删除元素所在结点 / 的兄弟结点中 / 值最小的移动到双亲结点中,再将双亲结点中小于且紧邻 / 该上移元素 / 的元素 / 移动到 / 删除元素所在结点
③若所在结点的元素个数和所在结点的所有紧邻兄弟结点的元素个数等于 ⌈ m / 2 ⌉ − 1 \lceil m/2\rceil - 1 ⌈m/2⌉−1//以上为条件
将与 / 双亲结点中 / 指向删除元素所在结点 / 的指针域 / 紧邻的元素,合并到 / 删除元素所在结点 / 的右(或左)兄弟结点中
(注意:将双亲结点移动到紧邻兄弟结点中相当于,先删除双亲结点中的某个元素,再在兄弟节点中添加这个元素,所以当双亲结点中只存在一个结点时就会产生双亲结点被删除的后果,那么这时候需要 / 继续按照删除元素的所有情况 / 对双亲结点的双亲结点进行上述三种操作,详情见书P245)
提醒:无论是删除还是插入,每一步都要检查操作后的树是否符合B树的性质,否则继续按照删除或插入情况继续进行操作
哈希表
时间复杂度: O ( 1 ) O(1) O(1)
哈希函数的构造方法
1、直接定址法:线性函数值作为哈希地址
2、数字分析法:将不敏感信息屏蔽
3、平方取中法:取关键字平方后的中间几位为哈希地址
4、折叠法:将关键字凤娥成位数相同的及部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址
5、除留余数法(重点):取关键字被某个不大于哈希表长m的数p除后所得的余数为哈希地址(说白了就是取模运算)(选p一般为质数或者不包含小于20的质因数的和数)
6、随机数法
处理冲突的方法
1、开放地址法(重点) :(哈希函数值+增量序列d)MOD 哈希表长L
其根据d的不同包含三种方式:
① d = 1 , 2 , 3 , … , L − 1 d=1,2,3,…,L-1 d=1,2,3,…,L−1:线性探测再散列
② d = 1 2 , 2 2 , … , k 2 ( k ≤ m 2 ) d=1^2,2^2,…,k^2(k\le \frac{m}{2}) d=12,22,…,k2(k≤2m):二次探测再散列
③ d = 伪随机数序列 d=伪随机数序列 d=伪随机数序列:伪随探测再散列
2、再哈希法:将第一次产生的哈希函数值放入另一种哈希函数中求出哈希函数值
3、链地址法:将所有关键字为同义词的记录存储在同一线性表中(类似于邻接表)
4、建立公共溢出区
七、内部排序
归并的比较次数更正: ( n l o g n 2 , n l o g n − n + 1 ) (\frac{nlogn}{2},nlogn-n+1) (2nlogn,nlogn−n+1)
插入排序
直接插入排序(稳定)
将原表中的元素从前往后依次遍历,将大的不断往前放
时间复杂度: O ( n 2 ) O(n^2) O(n2)
void InsertSort(int arr[], int len)
{
for (int i = 0; i < len; i++){
int end = i;
int curr = i + 1;
while (end >= 0){
if (arr[end] > arr[curr]){
arr[curr] = arr[end];
end--;
}
else
break;
}
arr[end + 1] = arr[curr];
}
}
折半插入
使用折半查找到指定位置然后插入
2-路插入排序
不重要不再赘述
详情可参考其他文章
希尔排序(不稳定)
说白了就是减少增量,在短增量中依旧使用还是类似于直插的方式(建议看一下书里的图,更直观)
时间复杂度:随间隔量改变而改变
void ShellSort(int arr[], int len)
{
int gap = len / 2;//自定义的
while (gap > 1){
for (int j = 0; j < gap; j++){
for (int i = 0; i < len - gap; i++){
int end = j + i;
int curr = arr[j + i + gap];
while (end >= 0){
if (arr[end] > curr){
arr[end + gap] = arr[end];
end -= gap;
}
else
break;
}
arr[end + gap] = curr;
}
}
gap -= 2;//自定义的
}
}
快速排序
冒泡排序(稳定)
时间复杂度: O ( n 2 ) O(n^2) O(n2)
void BubbleSort(int arr[], int len)
{
for (int i = 0; i < len - 1; i++){
for (int j = 0; j < len - i; j++){
if (arr[j] > arr[j + 1]){
int tem = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = tem;
}
}
}
}
快速排序(不稳定)
时间复杂度: O ( l o g n ) O(logn) O(logn)
本人采用递归算法(比较省事^ o ^)
int CreateCenter(int arr[], int low, int high)//求枢纽值并进行排序
{
arr[0] = arr[low];//arr[0]不要用来存储数据,其当作临时空间存储arr[low]
while (low < high){
while (low < high && arr[high] > arr[0])
high--;
arr[low] = arr[high];
while (low < high && arr[low] < arr[0])
low++;
}
arr[low] = arr[0];
return low;
}
void QuickSort(int arr[], int low, int high)
{
if (low < high){
int center = CreateCenter(arr, low, high);//求枢纽值并进行排序
QuickSort(arr, low, center - 1);//对左子表进行排序
QuickSort(arr, center + 1, high);//对右子表进行排序
}
}
选择排序
简单选择排序(不稳定)
时间复杂度: O ( n 2 ) O(n^2) O(n2)
就是很普通的在后面剩下的元素中找最小的放到前面去就可以完成从小到大的排序
int min (int arr[], int low, int high)
{
int min = INT_MAX;
for (int i = low; i <= high; i++){
if (arr[i] < min)
min = arr[i];
}
return min;
}
void SelectSort(int arr[], int len)
{
for (int i = 1; i <= len; i++){
int j = min(arr, i, len);//在i~len中找到最小的元素
if (i != j){//将最小的往前放置
int tem = arr[j];
arr[j] = arr[i];
arr[i] = tem;
}
}
}
树形选择排序(非重点不再赘述)
堆排序(不稳定)
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
比较次数: n l o g n nlogn nlogn
空间复杂度: l o g n logn logn
此处以最小堆(从小到大输出)作为例子,最大堆(从大到小输出)以此类推
最小堆解释: { k i ≤ K 2 i + 1 k i ≤ k 2 i ( i = 1 , 2 , 3 , … , ⌊ n 2 ⌋ ) \{^{k_i\le k_{2i}}_{k_i\le K_{2i+1}}(i=1,2,3,…,\lfloor \frac{n}{2}\rfloor) {ki≤K2i+1ki≤k2i(i=1,2,3,…,⌊2n⌋)
(如果看不懂的话可以参考本文二叉树性质⑤,本身属于完全二叉树编号的问题,这样去考虑问题就可以直接在原来记录数据的数组上进行操作而不用去另外构建二叉树)
建立时自下而上(从数组的右到左),输出过程中自上而下(从数组的左到右)
注意运用这种编号方式就不能使用arr[0]了!!!
void HeapAdjust(int arr[], int i, int len)
{
int tem = arr[i];//提前保存,防止之后的操作把它搞丢
for (int j = i * 2; j <=len; j *= 2){
if (arr[j] > arr[j + 1])//在两个孩子结点之中选一个最小的结点,方便下面去跟双亲比较
j++;
if (arr[i] <= arr[j])
break;
else{
arr[i] = arr[j];
i = j;//继续往下找,看看调换顺序后有没有影响到之前的大小排列
}
}
arr[i] = tem;
}
void HeapSort(int arr[], int len)
{
for (int i = len / 2; i > 0; i--){//调整成最小堆
HeapAdjust(arr, i, len);//建立最小堆
}
for (int i = len; i > 1; i--){
printf ("%d", arr[1]);//始终输出当前堆中的最小的值
arr[1] = arr[i];//输出后扔掉,把堆底挪上来
HeapAdjust(arr, 1, i - 1);//只改动了堆顶,所以只需要从1开始再调整为最小堆
}
}
归并排序(稳定)——2-路归并
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
说白了就是两两归并,直至归并为一个,有点类似于希尔排序(但是有区别,在于起始点不是连续的)
递归形式的2-路归并实用性很差,所以此处不写递归的代码
void Merge(int arr1[], int arr2[], int m, int k, int n)
//将arr1[m~k]和arr1[k~n]归并为arr2[m~n]
//arr2是辅助数组用来存储排好序的数据
//m是起点,k是中间点,n是终点
{
int i = m, j = k + 1;
for (; i <= k && j <= n; ){
if (arr1[m] < arr1[j])
arr2[i++] = arr1[m++];
else
arr2[i++] = arr1[j++];
}
for (; m <= k;)
arr2[i++] = arr1[m++];//将剩余的arr1[m~k]复制进去
for (; j <= n;)
arr2[i++] = arr1[j++];//将剩余的arr1[k~n]复制进去
}