三界
加法规则:O(g1)+O(g2) = O(max{g1,g2})
嵌套相乘:O(g1*g2)
一般大小比较:O(logn)
简单案例:
for(int j=1;j<=n;j*=2){}
//j每次都要乘2,设第k次跳出循环,则要2^k>n,求解k即可
//在某些案例中:++i比i++快!由于是先保留后计算和先计算后保留的区别
单向链表
案例:若要删去1个节点,如何删去p指向的c节点,将d复制给c,再删去d节点
a->b->c->d
双向链表
循环链表
深度:节点最大层次,从根节点开始
//求深度
int depth(node* p) {
//结束条件,都是因为上一层传进了左子树或右子树
if (p == NULL) return 0;
else {
int left = 0;
int right = 0;
left = depth(p->lc);//先一直找左子树,直到叶子节点时,才执行查找右子树并返回
right = depth(p->rc);
return left > right ? (left + 1) : (right + 1);
}
}
存储:
规律
先序
void preOrder(node* tree) {
if (tree == null) return;
cout << tree->ch;
preOrder(tree->lc);
preOrder(tree->rc);
}
中序
void inOrder(node* tree) {
if (tree == null) return;
preOrder(tree->lc);
cout << tree->ch;
preOrder(tree->rc);
}
后序
void postOrder(node* tree) {
if (tree == null) return;
preOrder(tree->lc);
preOrder(tree->rc);
cout << tree->ch;
}
广度优先
//队列
void BFS_tree(binTreeNode* p) {
queue q;
q.push(p);
binTreeNode* temp=null;
while(!q.empty){
temp=q.front();//取前节点
visit(temp);//输出前节点的全部节点!!!
q.pop();//将前节点弹出
//左右孩子非空
if(temp->left!=null) q.push(temp->left);
if(temp->right!=null) q.push(temp->right);
}
}
深度优先(非递归)
//栈,递归可用先序
//进栈的同时访问,到最底层后出栈返回
void DFS_tree(binTreeNode* p) {
stack s;
s.push(p);
binTreeNode* temp=null;
while(!s.empty()){
temp=stack.top();//取最高的节点
visit(temp);//输出节点的其中一个节点
stack.pop();//将已输出的弹出
//左右孩子非空
if(temp->left!=null) stack.push(temp->left);
if(temp->right!=null) stack.push(temp->right);
}
}
规则:左子树比根小,右子树比根大
注意中序遍历得到的数据
删除操作!
void remove(node* root, int key) {
if (root == NULL) return;
else if (key < root->data) remove(root->left, key);
else if (key > root->data) remove(root->right, key);
else {//等于,即为找到!
if (root->left == NULL) root = root->right;//被删节点若左孩子为空,直接用右孩子补上
else if (root->right == NULL) root = root->left;
else {//左右孩子都非空,此时可以找被删节点左孩子中最大或右孩子中最小进行补
node* temp = findv(root->right, root);
root->data = temp->data;
delete temp;
}
}
}
node* findv(node* root, node* preRoot) {//右孩子中最小
if (root->left == NULL) {
if (root->right != NULL) preRoot->left = root->right;//要往上提的节点若非叶子时
return root;
}
findv(root->left, root);
}
查找操作
bool search(node* root,int key){
while(root!=null){
if(key==root->data) return true;
else if(keydata) root=root->left;
else root=root->right;
}
return false;
}
插入操作
//非递归
void insert(int key, node* root) {
node* prev=null;//定位最终插入位置的父节点
//1.先定位
while(root!=null) {
prev=temp;
if(key < temp->data) temp=temp->left;
else if(key > temp->data) temp=temp->right;
else return;
}
//2.再插入
if(key < prev->data) {//创建新节点}
else{//创建新节点}
}
//递归(不严谨)
void insert(int key, node* root) {
if(root==null) {
//先创建new,赋值key
return;
}
if(key < root->data) {
insert(key,root->left)
}else {
insert(key,root->right)
}
return;
}
基本概念
小顶堆(根最小)
下面的 siftdown函数 和 sift_heap 函数一样原理
建立堆(数组中)
void sift_heap(int a[], int parent, int last) {
for (int lc = parent * 2 + 1; lc <= last; lc = lc * 2 + 1) {
//左,右节点最小的一个
if (a[lc] < a[lc + 1] && lc + 1 <= last) lc++;
//如果父节点比孩子大就交换,且孩子的索引要小于等于数组的元素个数(最后的一个叶子)
if (a[parent] < a[lc]) {
swap(a[parent], a[lc]);
parent = lc;//当前层的孩子作为下一层的父节点
}
else break;//不需要交换
}
}
void build_heap(int a[], int nums) {
for (int node = nums / 2 - 1; node >= 0; node--) {//从最后一个非叶子节点node开始
sift_heap(a, node, nums - 1);
}
}
堆删除元素
bool remove(int pos) {
if(pos=<0||pos=>=n) return false;
swap(heap[pos],heap[--n]);//将要删除的元素放在数组的最后(即是最后一个叶子)
while(pos!=0 && heap[pos]left != null || heap[pos]->right != null) {//不是叶子
//将当前节点和左右孩子比较
int lc=2*pos+1;
int rc=2*pos+2;//注意要满足是否有右孩子,rcheap[rc]) lc=rc;//rc更小,更小的和父节点比较
if(heap[pos] <= heap[lc]) return;
swap(heap[pos],heap[lc]);
pos=lc;
}
}
堆插入
bool insert(int key) {
if(pos_index >= size) return false;//堆满时
int current=pos_index++;//在数组中,先放在最后一个
heap[current]=key;
//开始调整堆,保证 插入节点的父节点 比key要小
while( current!=0 && heap[current]
大顶堆(根最大):与小顶堆类似
利用结构数据储存,越靠近叶子,权值越低
每次从哈夫曼编码中,选择两个权值最小的编码进行创建节点(两个合并一个后继续加入原编码中继续比较!)
建立二叉树后,从根节点遍历,左孩子路径上一般为0,右孩子一般为1
简单案例
//假设有n个哈夫曼编码(注意数组中index=0不存!)
struct node {
int parent = 0;//判断是否已经合并
int lc=0;
int rc=0;
char ch=0;//编码
int weight=0;//权值
};
void huffman() {
node* arr=new node[2*n];//n个编码,2n-1数组大小,因为两个合并为一个后,也要放进数组中
//假设arr的前n个已经存有编码信息(index=0不存放),开始创建
for (int i = n + 1; i <= 2 * n - 1; ++i) {
int index1 = 0;
int index2 = 0;
selectMin(arr, i - 1, index1, index2);//从arr数组中选两个权值最小的,从1到i-1之间
arr[index1].parent = i;
arr[index2].parent = i;//parent被标记,说明已经合并
arr[i].lc = index1;
arr[i].rc = index2;
arr[i].weight = arr[index1].weight + arr[index2].weight;
}
}
//求每个编码的路径边数,也就是重新分权值,1到n,n个编码
for(int i=1; i<=n; i++) {
int j=i;
while (arr[j].parent != 2*n-1) {//此时索引2n-1是整棵树的根节点!
++count;//边数,从0开始
j = arr[j].parent;//从叶子往根节点找!
}
}
数组已排序
比较次数 <= 树的深度[log(2)n]+1(2为底数)
不停通过与中间的元素比较进行减半查找
//非递归
bool bin_search(int key) {//已知数组arr
int low=0;
int high=arr.length;
while(low<=arr.length){
int mid=(low+high)/2;
if(arr[mid]==key) return true;
else if(arr[mid]
基本概念:
要求规则
查找:2-3树是3叉树,高度为θ(log(3)n)
//假设节点中左值名为lkey,右值名为rkey,根节点root,查找key
if(root==null){}
if(key==lkey){}
if(root->rkey!=null && key==rkey){}
if(key
2-3树的插入
三种情况
简单思路
//假设节点中左值名为lkey,右值名为rkey,根节点root,查找key,returnp是叶子满时返回父节点的一个节点
bool insert(node* returnp,……){
if(root==null){}
else if(isLeaf(root)){
if(root->rkey==null){}//与插入rkey处重新排序
else{splitNode(returnp,……);}//叶子满分裂
}
else if(keylkey){}//递归左
else if(root->rkey==null || keyrkey){}//递归中间孩子
else{}//递归右
if(returnp!=null){//说明叶子满
if(root->rkey!=null){splitNode(returnp,……);}//此时root为父节点
else{//插入右值,调整父节点指针指向
if(returnp->datalkey){}//data为返回节点的数据
else{}
}
}
}
//分裂,简单说明
bool splitNode(node* retptr,int key,……){//key为插入值,retptr为返回的节点
if(key
深度优先DFS【递归】
每次选领接顶点时,有一定规则(如选所有领接节点中数据最小的优先DFS)
每次进入递归时,从当前顶点遍找到所有的领接节点,逐个递归,并且检测当前顶点是否已经访问
简单案例
//假设n个顶点的名称为0,1,2,……,n-1
struct GraphMatrix
{
int** matrix;//领接矩阵
int* mark;//记录每个节点是否被访问,初始化全0,mark[0]=1,表示顶点0已被访问
};
void DFS_graph(GraphMatrix g,int peak,int n){
cout<
广度优先BFS【队列】
每次从头顶点开始,当前顶点的邻居全部入队
当队列不为空时,出队一个顶点,并访问这个顶点,该顶点的 不被访问过的邻居顶点 入队
简单案例
//假设n个顶点的名称为0,1,2,……,n-1
struct GraphMatrix
{
int** matrix;//领接矩阵
int* mark;//记录每个节点是否被访问,初始化全0,mark[0]=1,表示顶点0已被访问
};
void BFS_graph(GraphMatrix g,int peak,int n){
deque queue;//队列
queue.push_back(peak);
while(!queue.empty()){//非空
int front=queue.front();
q.pop_front();//出队
cout<
基本概念
简单应用:判断是否有环
//在领接矩阵中,有向图,这里设:行为出度,列为入度
deque queue_v;//顶点队列
int* degree = new int[v_nums + 1];//顶点入度数组
int** a = new int* [v_nums + 1];//顶点标号为1到n,v_nums为顶点个数
//计算每个顶点的入度
for (int i = 1; i <= v_nums; i++){
for (int j = 1; j <= v_nums; j++) {
if (a[j][i] == 1) degree[i]++;//按列查找入度
}
if (degree[i] == 0) queue_v.push_back(i);//0入度的节点入队!!!
}
//0入度的节点出队,入度减1
while (!queue_v.empty()){
int index = queue_v.front();
for (int i = 1; i <= v_nums; i++) {//找到该节点的出度指向,并且被指向的节点,入度减1
if (a[index][i] == 1) degree[i]--;
if (degree[i] == 0) queue_v.push_back(i);//如果入度减少后,等于0,就入队
}
queue_v.pop_front();//出队
}
//查找入度数组是否全为0
int nums = 0;
for (int i = 1; i <= v_nums; i++) {
if (degree[i] != 0) nums++;//若还有顶点入度不为0,则说明有环
}
Floyd算法(多元顶点)
思路(n个顶点):
简单案例
//图结构参考Dijkstra算法
void Floyd(GraphMatrix g) {
for(int k=1; k<=n; k++) {//中转点
for(int i=1; i<=n; i++) {//从一个顶点出发经过中转点
for(int j=1; j<=n; j++) {
if(g.matrix[i][j] > g.matrix[i][k] + g.matrix[k][j]) {
g.matrix[i][j] = g.matrix[i][k] + g.matrix[k][j];
}
}
}
}
}
Dijkstra算法(单元顶点)
思路:求源点到终点的最短路径
简单案例
//假设n个顶点的名称为1,2,……,n
struct GraphMatrix
{
int** matrix;//领接矩阵,无法到达用INT_MAX表示
int* mark;//记录每个节点是否被访问,初始化全0,mark[1]=1,表示顶点1已被访问
};
//找最短路径数组最小的(除访问过外)
int search_min_index(GraphMatrix g, int* shortest_path, int n , int begin) {
int min = INT_MAX;
int index = 0;
for (int i = 1; i <= n; i++)
{
//找最小时,既要排除已经标志的点,又要不能等于自身!
if (shortest[i] < min && i != begin && g.mark[i]!=1) {
min = shortest[i];
index = i;
}
}
return index;//返回当前最小路径的顶点
}
void Dijkstra(GraphMatrix g, int n, int begin, int target) {
//最短路径初始化
int* shortest_path = new int[n + 1];//建立最短路径数组,顶点名称是1到n,直接用索引替代
shortest_path[begin]=0;
for(int i=1; i<=n; i++) {
if(i!=begin) shortest_path[i]=INT_MAX;
}
//标记是否被访问的数组 初始化
g.mark[begin]=1;
//从源点出发,找到与其相邻的顶点,进行最短路径更新
for(int i=1; i
Prim算法
思路:
局部贪心但全局最优;从点出发扩边
简单案例
#define max 100
#define Weight_max 10000
//顶点名称从1到n,这里默认顶点1为源点!
void Prim(int g[][max], int v_num) {
//weight_Arr[j],记录在邻接矩阵中第j列的权重g[i][j]的值,当值为-1说明已经在最小生成树里面
int weight_Arr[max];
//parent_Vex[i],记录weight_Arr[j]的父顶点,也就是g[i][j]中的i——若没有父顶点,则为-1
int parent_Vex[max];
int min, sum=0, min_Vex=1;
for (int i = 2; i <= v_num; i++) {
weight_Arr[i] = g[1][i];//注意初始化,全部赋值
parent_Vex[i] = 1;//本函数的源点!下标为1开始
}
parent_Vex[1] = -1;//由于下标为1时是源点,则源点没有父顶点,所以赋值为-1
for (int i = 2; i <= v_num; i++) {
min = Weight_max;//初始化
min_Vex = 1;//初始化
for (int j = 2; j <= v_num; j++) {
//找到权重最小的顶点,且没进入生成树
if (weight_Arr[j] < min && weight_Arr[j] != -1) {
min = weight_Arr[j];
min_Vex = j;//【重点】找到最小权值的边后,将这条边的终点作为下一次查找的开始
}
}
sum += min;
weight_Arr[min_Vex] = -1;//赋值为-1,说明已经进入生成树!
for (int i=2; i <= v_num; i++) {
//更新表,看 以新的顶点到达某点A 与 旧的顶点 到达顶点A 哪个权值小(权值大不换)
if (g[min_Vex][i] < weight_Arr[i]) {
weight_Arr[i] = g[min_Vex][i];
parent_Vex[i] = min_Vex;//修改父顶点
}
}
}
cout << "最小生成树权值之和=" << sum;
}
Kruskal算法
思路
多点孤立,选取最短边;每次考虑是否会构成环
简单案例
//边结构,记录边的两个顶点,已经权值
struct Edge
{
int vex_origin;
int vex_finish;
int edge_weight;
};
//假设结构数组e已经按照权值,从小到大排序
void Kruskal(Edge e[], int v_num, int e_num) {
int i, j, origin, finish, sum = 0;
int sn1, sn2;
int* vest = new int[v_num + 1];
//用来记录已经加入最小生成树中的边能到达的最远源点,初始化说明只能到达自身!
for (i = 1; i <= v_num; i++) {
vest[i] = i;
}
int k = 1;
j = 1;
while (k < v_num)//比顶点数小1
{
origin = e[j].vex_origin;//取出最小边的起点
finish = e[j].vex_finish;//最小边的终点
sn1 = vest[origin];//作为一个源点
sn2 = vest[finish];//选出能到达最远的源点,如finish顶点能到达源点sn2
if (sn1 != sn2) {//如果能到达最远的源点相同,不是重合就是回路
sum += e[j].edge_weight;
k++;
for (i = 1; i <= v_num; i++) {
if (vest[i] == sn2) {
vest[i] = sn1;//更换源点
}
}
}
j++;
}
cout << "最小生成树权值之和=" << sum;
}
数组中遍历每一个元素,其中每个元素a[k]与前面元素作比较;直到比前一个元素大或小或者已到达a[0]
简单案例(从小到大)
void insert_sort(int a[],int n){
for(int i=1; i0 && a[j]
两两交换(相邻),遍历每个元素,每次范围为[i, n-1]
可添加标号mark,标记某一次是否已经有序
简单案例(从小到大)
void bubble_sort(int a[],int n) {
for(int i=0; ii!
int mark=0;
for(int j=n-1; j>i; j--) {//每次从尾部向上冒泡,最小的一定先出现最上面,所以每次比较范围缩小
if(a[j]
循环,每次选择 非排序的元素中最小的元素 与前面交换
简单案例(从小到大)
void select_sort(int a[],int n) {
for(int i=0; ia[j]) min=j;//下标转移
}
swap(a[min],a[i]);
}
}
思想:将数组分为多个子序列,子序列排序后,再合并
每个子序列分散在各组中,间隔首先取n/2。如:a[0]与a[2]是一组;a[1]与a[3]是一组…然后间隔继续缩小直到为1!
注意:不稳定排序,分组时,不同子序列的排序会破环两个相同元素的相对位置!
时间复杂度:O(n^(3/2))
简单案例(从小到达)
void shell_sort(int a[],int n) {
for(int i=n/2; i>=1; i=i/2) {//间隔不断减少,直到1!!!
for(int j=0; jfront;1->interval
void insert_sort(int n, int front, int interval) {
for(int i=front+interval; ifront && a[j]
思想:选择一个支点,比支点小在左,比支点大在右(从小到大);左右两边继续快排
时间复杂度:O(nlogn)
简单案例(从小到达)
//1、快排支点quickPivot,根据数组的头部和尾部自行确定支点,这里直接取尾部!
int quickPivot(int l, int r) {
return r;
}
//2、快排调换位置quickSwap
int quickSwap(int a[], int l, int r) {
int pivot_index = quickPivot(l, r);//支点索引
int last = r;//数组最后的一个数据索引
int pivot = a[pivot_index];
//将支点放在最后,确保不论根据哪个位置的值进行左右分治,都能连续地从l到r-1
swap(a[last], a[pivot_index]);
while (l < r) {
while (a[l] < pivot) l++;//等于时交换
while (a[r] >= pivot && r > l) r--;//等于时不交换
swap(a[l], a[r]);
}
//将原本的支点pivot归位,这时l==r!
//索引比l小的,比a[l]小;索引比l大,比a[l]大;
swap(a[last], a[l]);
return l;
}
//3、函数quicksort
void quicksort(int a[], int l, int r) {
if (r < l) return;
int pivot = quickSwap(a, l, r);
//根据参数l和r进行递归
quicksort(a, l, pivot - 1);//支点左
quicksort(a, pivot + 1, r);//支点右
}
完全二叉树(不存在只有右孩子这种情况!),数组存储
思想:每次对堆顶去除最小(最大)的元素,并将堆顶元素放在数组最后;重新得到新堆顶;执行n-1次
时间复杂度:O(nlogn)
简单案例(从小到大)
//建立大顶堆
void sift_heap(int a[], int parent, int last) {
for (int lc = parent * 2 + 1; lc <= last; lc = lc * 2 + 1) {//完全二叉树,左孩子先
if (a[lc] < a[lc + 1] && lc + 1 <= last) lc++;//左,右节点最小的一个
if (a[parent] < a[lc]) {//父节点比较
swap(a[parent], a[lc]);
parent = lc;
}
else break;
}
}
void heap_sort(int a[], int nums) {
for (int node = nums / 2 - 1; node >= 0; node--) {//从最后一个非叶子节点node开始
sift_heap(a, node, nums - 1);
}
//从最后一个元素开始,不停和a[0]进行交换,已经交换的堆积在数组尾部,所以是头不变,尾部不停减1
for (int i = nums - 1; i > 0; i--)
{
swap(a[0], a[i]);
sift_heap(a, 0, i - 1);//从根节点index=0,一直往下重新筛选!
}
}
一般是二分序列,将二分序列合并一个序列,递归合并;当序列长度为1时,返回合并
时间复杂度:O(nlogn)
缺点:空间利用多,适用于外排
优化:二分序列中,可采用一个序列从小到大,另一个序列从大到小,避免比较
简单案例(从小到大)
void merge_sort(int a[], int temp[], int left, int right) {
if (right <= left) return;
int mid = (left + right) / 2;//二分
merge_sort(a, temp, left, mid);//左序列
merge_sort(a, temp, mid + 1, right);//右序列
//开始左、右序列合并
for (int i = left; i <= right; i++) temp[i] = a[i];//先复制,从temp比较重新写进a!
int Lhead = left;//左序列第一个元素索引
int Rhead = mid + 1;//右序列第一个元素索引
for (int cur = left; cur <= right; cur++) {
if (Lhead > mid) a[cur] = temp[Rhead++];//左序列已经访问完,但右序列还有
else if (Rhead > right) a[cur] = temp[Lhead++];//右序列已经访问完,但左序列还有
else if (temp[Lhead] > a[Rhead]) a[cur] = temp[Rhead++];
else a[cur] = temp[Lhead++];
}
}
以0-9作为一个箱子的基准(个,十,百位……)
思想:
简单案列(了解)
//最高有多少位
int maxBit(int data[], int n) {
//先找最大数
int max = data[0];
for (int i = 1; i < n; i++) {
if (max < data[i]) max = data[i];
}
//数有多少位
int bit = 1;
while (max >= 10) {
max = max / 10;
++bit;
}
return bit;
}
//基数排序
void radixSort(int data[], int n) {
int bit = maxBit(data, n);
int* temp = new int[n];//辅助存储每一次提出桶后的数据
int* count = new int[10];//计数器(桶或箱),无论个十百位,每一位都是0-9!
int radix = 1;//记录第几位!!
//最高多少位,就要排多少次
for (int i = 1; i <= bit; i++) {
//清空计数器,每位入桶都不一样
for (int j = 0; j < 10; j++) {
count[j] = 0;
}
//统计每个桶中的数据个数
for (int j = 0; j < n; j++) {
int k = (data[j] / radix) % 10;//每次都取最后一位!!!
//radix为1取个位,radix为10取十位
count[k]++;
}
//算出每个数据在temp中的位置,如计数排序中一样
//后面的数据出现的位置为前面所有数据出现的次数之和;这样才能对应排序后数组中的位置下标
for (int j = 1; j < 10; j++) {
count[j] = count[j] + count[j - 1];
}
//将桶中数据记录到temp中
for (int j = n - 1; j >= 0; j--) {
int k = (data[j] / radix) % 10;//每次都取最后一位!!!
temp[count[k] - 1] = data[j];//注意-1,将次数换为从0开始的下标
count[k]--;//次数减一,相同的数值向前移一位
}
//复制
for (int j = 0; j < n; j++) {
data[j] = temp[j];
}
//再下一位
radix = radix * 10;
}
}
计数排序统计小于等于该元素值的元素的个数i,于是该元素就放在目标数组的索引i位(i≥0)
基于一个假设,待排序数列的所有数均为整数,且出现在(0,k)的区间之内。如果 k(待排数组的最大值)过大则会引起较大的空间复杂度,一般是用来排序 0 到 100 之间的数字
简单案例
struct elemType //每个记录(元素)的结构
{
keyType key;
};
struct sqList //顺序表结构
{
elemType r[maxsize + 1]; //下标为0不放元素,从1开始,r[0]一般作为哨兵或缓冲区!
int length; //记录元素的个数,不是数组长度
};
//计数排序
void countSort(sqList& l, sqList& after) {
//找出最大最小的元素
int max = l.r[1].key;
int min = l.r[1].key;
for (int i = 1; i <= l.length; i++) {
if (l.r[i].key > max) max = l.r[i].key;
if (l.r[i].key < min) min = l.r[i].key;
}
const int countLength = max - min + 1;//1号下标存放元素
int* count = new int[countLength]; //创建记录出现次数的数组
for (int i = 0; i <= countLength - 1; i++) {
count[i] = 0;
}
//统计出现的次数
for (int i = 1; i <= l.length; i++) {
count[l.r[i].key - min]++; //减去min是下标偏移!
}
//后面的键值出现的位置为前面所有键值出现的次数之和;这样才能对应排序后数组中的位置下标
//比如 1 1 2 2 3;1和2出现两次,所以3再第5个下标
for (int i = 1; i <= countLength - 1; i++) {
count[i] = count[i] + count[i - 1];
}
//将数组放到目标位置,倒序!!!
for (int i = countLength - 1; i >= 1; i--) {
//注意自减符号的位置!输出说明次数减一!相同元素往前移一位
after.r[count[l.r[i].key - min]--] = l.r[i];
}
}