0.算法和算法分析
- 算法时间效率
算法时间效率可以用依据该算法编制的程序在计算机上执行所消耗的时间来度量;
* 事后统计:实验;依赖于实验的条件;
* 事前分析:程序分析
一个算法的运行时间是指一个算法在计算机上运行所耗费的时间大致可以等于计算机执行一种简单操作(比如:赋值比较,移动)所需要的时间与算法中进行简单操作次数的乘积;
算法运行时间=一个简单操作所需要的时间x简单操作次数
即:
算
法
运
行
时
间
=
∑
每
条
语
句
执
行
的
次
数
x
该
语
句
执
行
一
次
所
需
要
的
时
间
;
算法运行时间=\sum每条语句执行的次数x该语句执行一次所需要的时间;
算法运行时间=∑每条语句执行的次数x该语句执行一次所需要的时间;
每条语句执行的次数称为语句频度,所以上式等价:
算
法
运
行
时
间
=
∑
每
条
语
句
频
度
x
该
语
句
执
行
一
次
所
需
要
的
时
间
;
算法运行时间=\sum 每条语句频度x该语句执行一次所需要的时间;
算法运行时间=∑每条语句频度x该语句执行一次所需要的时间;
每一条语句执行的时间,一般随机器而异,取决于机器指令性能,速度以及编码的代码质量,是由机器本身硬件环境决定与算法无关,所以,我们可以假设执行每局语句所需要的时间均为单位时间,所以我们只需要关注频度之和;
//例如:执行两个nxn矩阵相乘的算法
for(i=1;i<=n;i++) //条件判断需要执行n+1次,循环体只需要执行n次
{
for(j=1;j<=n;j++) //n(第一层循环)x(n+1)(条件判断需要执行n+1次)==>n(n+1)
{
c[i][j]=0; //nxn次
for(k=0;k<n;k++) //nxnx(n+1)次
{
c[i][j]=c[i][j]+a[i][k]*b[k][j];//nxnxn次
}
}
}
//所以总共需要执行:T(n)=2n^3+3n^2+2n+1
由上面的例子知道:程序所需要执行的时间为:
T
(
n
)
=
2
n
3
+
3
n
2
+
2
n
+
1
T(n)=2n^3+3n^2+2n+1
T(n)=2n3+3n2+2n+1
为了便于比较不同算法的时间效率,我们仅仅比较他们的数量级;
定义如下:
若某个辅助函数f(n)
,使得当n
趋近无穷大时,T(n)/f(n)
的极限为一个不等于0的常数,则称f(n)
是T(n)
的同数量级函数,记作:T(n)=O(f(n))
,称O(f(n))
为算法的渐进时间复杂度(O是数量级的符号),简称时间复杂度;
所以上式的时间复杂度可以通过下式计算:
T
(
n
)
=
2
n
3
+
3
n
2
+
2
n
+
1
T(n)=2n^3+3n^2+2n+1
T(n)=2n3+3n2+2n+1
f
(
n
)
=
n
3
f(n)=n^3
f(n)=n3
T
(
n
)
f
(
n
)
=
lim
n
→
∞
2
n
3
+
3
n
2
+
2
n
+
1
n
3
=
2
\frac{T(n)}{f(n)}=\lim_{n \to \infty} \frac{2n^3+3n^2+2n+1}{n^3}=2
f(n)T(n)=n→∞limn32n3+3n2+2n+1=2
所以 O(f(n))=
O
(
n
3
)
O(n^3)
O(n3)
所以由上式可以看出,算法的时间复杂度之和频度最高语句有关,所以计算时间复杂度时,不必计算所有操作的执行次数,而只考虑算法中基本操作的执行次数,它是问题规模n的某个函数,用T(n);
算法中基本语句重复执行的次数是问题规模n的某个函数f(n),算法的时间量度记作:T(n)=O(f(n))
;
它表示随着n的增大,算法执行的时间的增长率和f(n)的增长率相同,称为渐进时间复杂度;
基本语句:
- 执行重复次数最多
- 算法中重复执行次数和算法的执行时间成正比的语句
- 对算法运行时间的贡献最大
问题规模n:
n越大算法执行时间越长; - 排序:n为记录数
- 矩阵:n为矩阵阶数
- …
定理:
若
f
(
n
)
=
a
m
n
m
+
a
m
−
1
n
m
−
1
+
.
.
.
+
a
1
n
+
a
0
,
则
T
(
n
)
=
O
(
n
m
)
若f(n)=a_{m}n^m +a_{m-1} n^{m-1}+...+a_{1}n+a_{0},则T(n)=O(n^m)
若f(n)=amnm+am−1nm−1+...+a1n+a0,则T(n)=O(nm)
- 找出语句频度最大的那条语句作为基本语句;==>一般找循环体中嵌套最深的语句,计算其执行次数;
- 计算基本语句的频度得到问题规模n的某个函数f(n);
- 取其数量级符号"O"
x=0;y=0;
for(int k=0;k<n;k++)//条件判断执行n+1次
{
x++; //循环体执行n次
}
for(int i=0;i<n;i++)//条件判断执行n+1次
{
for(int j=0;j<n;j++)//n(外层循环体执行次数)xn+1次;
{
y++;//nxn
}
}
//所以最大执行次数为:n(n+1)==>n^2(忽略常数项)
void exam(float x[][],int m,int n)
{
float sum[];
for(int i=0;i<m;i++)
{
sum[i]=0.0;
for(int j=0;j<n;j++)
{
sum[i]+=x[i][j]; //f(n)=mxn;==>T(n)=O(mxn);
}
for(i=0;i<m;i++)
{
cout<<"";
}
}
}
对于很难直接统计计算次数的可以用级数求和:
void A()
{
for(i=1;i<=n;i++)
{
for(j=1;j<=i;j++)
{
for(k=1;k<=j;k++)
x=x+1;
}
}
}
所以A函数的复杂度为:
语
句
频
度
=
∑
i
=
1
n
∑
j
=
1
i
∑
k
=
1
j
=
∑
i
=
1
n
∑
j
=
1
i
j
=
∑
i
=
1
n
i
(
i
+
1
)
2
=
1
2
(
∑
i
=
1
n
i
2
+
∑
i
=
1
n
i
)
=
1
2
(
n
(
n
+
1
)
(
2
n
+
1
)
6
+
n
(
n
+
1
)
2
=
n
(
n
+
1
)
(
n
+
2
)
6
)
=
=
>
T
(
n
)
=
O
(
n
3
)
语句频度=\sum_{i=1}^{n}\sum_{j=1}^{i} \sum_{k=1}^{j} = \sum_{i=1}^{n}\sum_{j=1}^{i} j=\sum_{i=1}^{n}\frac{i(i+1)}{2}=\frac{1}{2}(\sum_{i=1}^{n} i^2+ \sum_{i=1}^{n} i) =\frac{1}{2}(\frac{n(n+1)(2n+1)}{6}+\frac{n(n+1)}{2}=\frac{n(n+1)(n+2)}{6})==>T(n)=O(n^3)
语句频度=i=1∑nj=1∑ik=1∑j=i=1∑nj=1∑ij=i=1∑n2i(i+1)=21(i=1∑ni2+i=1∑ni)=21(6n(n+1)(2n+1)+2n(n+1)=6n(n+1)(n+2))==>T(n)=O(n3)
执行次数不确定时:
i=1;
while(i<=n)
{
i*=2;
}
此题关键是要找到执行次数与n的关系,并表示成n的函数;
若
循
环
执
行
了
1
次
:
i
=
i
∗
2
=
2
;
若循环执行了1次:i=i*2=2;
若循环执行了1次:i=i∗2=2;
若
循
环
执
行
了
2
次
:
i
=
i
∗
2
=
2
2
;
若循环执行了2次:i=i*2=2^2;
若循环执行了2次:i=i∗2=22;
若
循
环
执
行
了
3
次
:
i
=
i
∗
2
=
2
3
;
若循环执行了3次:i=i*2=2^3;
若循环执行了3次:i=i∗2=23;
若
循
环
执
行
了
x
次
:
i
=
i
∗
2
=
2
x
;
若循环执行了x次:i=i*2=2^x;
若循环执行了x次:i=i∗2=2x;
所
以
由
循
环
条
件
i
<
=
n
;
=
=
>
2
x
<
n
=
=
>
x
<
log
2
n
=
=
lg
n
所以由循环条件i<=n;==>2^x<n==>x<\log_{2}{n}==\lg_{}{n}
所以由循环条件i<=n;==>2x<n==>x<log2n==lgn
所
以
该
段
程
序
的
复
杂
度
为
:
T
(
n
)
=
O
(
log
2
n
=
=
lg
n
)
所以该段程序的复杂度为:T(n)=O(\log_{2}{n}==\lg_{}{n} )
所以该段程序的复杂度为:T(n)=O(log2n==lgn)
所以:时间复杂度是由嵌套最深的语句的频度或执行次数最多的语句决定的;;
有的时候,算法基本操作重复执行的次数还与问题的输入数据集不同而不同;
//顺序查找,在数组a[i]中查找值等于e的元素,返回其所在位置
for(i=0;i<n;i++)
{
if(a[i]==e) return i+1;
return 0;
}
//此时算法执行循环的次数就不一定是n,最好情况下,执行一次就返回,最坏执行n次返回;
所以对于一般情况下,需要计算算法的算法最坏时间复杂度
,最好时间复杂度
以及平均时间复杂度
;
对于复杂算法,可以将它分成几个容易估算的部分,然后利用加法法则和乘法法则,计算算法时间复杂度:
- 加法法则
T ( n ) = T 1 n + T 2 n = O ( f ( n ) ) + O ( g ( n ) ) = O ( m a x ( f ( n ) , g ( n ) ) ) T(n)=T_{1}n+T_{2}n=O(f(n))+O(g(n))=O(max(f(n),g(n))) T(n)=T1n+T2n=O(f(n))+O(g(n))=O(max(f(n),g(n))) - 乘法法则
T ( n ) = T 1 n x T 2 n = O ( f ( n ) ) ∗ O ( g ( n ) ) = O ( m a x ( f ( n ) ∗ g ( n ) ) ) T(n)=T_{1}nxT_{2}n=O(f(n))*O(g(n))=O(max(f(n)*g(n))) T(n)=T1nxT2n=O(f(n))∗O(g(n))=O(max(f(n)∗g(n)))
常数阶 | 对数阶 | 线性阶 | 线性对数阶 | 平方阶 | 立方阶 | … | K次方阶 | 指数阶 | … |
---|---|---|---|---|---|---|---|---|---|
O(1) | O ( log 2 n ) O(\log_{2}n) O(log2n) | O(n) | O ( n log 2 n ) O(n\log_{2}n) O(nlog2n) | O ( n 2 ) O(n^2) O(n2) | O ( n 3 ) O(n^3) O(n3) | … | O ( n k ) O(n^k) O(nk) | O ( 2 n ) O(2^n) O(2n) | … |
空间复杂度:算法所需存储空间的度量,记作:S(n)=O(f(n)),其中n为问题的规模(或大小)
//将一维数组a中的n个数逆序放在原数组中
for(i=0;i<n/2;i++)
{
t=a[i];//只需要一个辅助空间;==>S(n)=O(1);
a[i]=a[n-i-1];
a[n-i-1]=t;
}
for(i=0;i<n;i++>)
{
b[i]=a[n-i-1];//需要一个辅助数组==>S(n)=O(n)
}
for(i=0;i<n;i++)
{
a[i]=b[i];
}
1.链表
2.静态链表
3.顺序栈
通过数组实现;
#include <iostream>
#include <random>
#include <limits.h>
using namespace std;
//写模板时,不仅可以传递类型,而且还可以传递,初始化大小;
template<class T,std::size_t ArrSize>
class stackArray
{
private:
int top = -1;
public:
stackArray();
void push_back(T data);
void pop();
bool empty();
T arr[ArrSize] = {};
int count = 0;
};
template<class T,std::size_t ArrSize>
stackArray<T,ArrSize>::stackArray()
{
}
template<class T,std::size_t ArrSize>
void stackArray<T,ArrSize>::push_back(T data)
{
static int insertNum = 0;
if (count == ArrSize)
{
cout << "栈已满" << endl;
return;
}
else
{
count++;
top++;
arr[top] = data;
cout << "插入第" << insertNum << "个元素:" << arr[top] << endl;
insertNum++;
return;
}
}
template<class T,std::size_t ArrSize>
void stackArray<T,ArrSize>::pop()
{
static int deleteNum = 0;
if (stackArray<T,ArrSize>::empty())
{
cout << "数据为空" << endl;
return;
}
else
{
cout << "删除第" << deleteNum << "个元素:" << arr[top] << endl;
arr[top] = {};
top--;
count--;
deleteNum++;
}
}
template<class T,std::size_t ArrSize>
bool stackArray<T,ArrSize>::empty()
{
if (count == 0)
return true;
else
return false;
}
int main()
{
default_random_engine e(time(0));
uniform_int_distribution<int> u(0, 10000);
stackArray<string,100> mstack;
for (int i = 0; i < 10; i++)
{
//+s:for string test
mstack.push_back(to_string(u(e))+"s");
//mstack.push_back(u(e));
cout<<"after push_bacK arr size:"<<mstack.count<<endl;
}
for (int i = 0; i < 10; i++)
{
mstack.pop();
cout<<"after pop arr size:"<<mstack.count<<endl;
}
return 0;
}
4.链式顺序栈
通过链表实现实现的顺序栈;
#include <iostream>
#include <random>
using namespace std;
//----------------for list ---------------------
template<class T>
struct Node
{
T data;
Node *next;
};
//----------------for stack ---------------------
template<class T>
class stackList
{
public:
Node<T> *top, *head;//需要采用头插法list,即新插入的元素,放置在链表前面.否则无法pop;
stackList();
void push_back( T num);
int count;
void pop();
bool empty();
private:
};
template<class T>
stackList<T>::stackList() : top(nullptr), head(nullptr), count(0)
{
}
template<class T>
void stackList<T>::push_back(T num)
{
static int indexpush = 0;
count++;
top = new Node<T>;
if (head == nullptr)
{
head = top;
top->data = num;
cout << "插入第" << indexpush << "个元素:" << top->data << endl;
top->next = nullptr;
}
else
{
top->data = num;
cout << "插入第" << indexpush << "个元素:" << top->data << endl;
top->next = head;
head = top;
}
indexpush++;
}
template<class T>
void stackList<T>::pop()
{
if (stackList::empty())
return;
else
{
static int index = 0;
count--;
Node<T> *temp = top;
cout << "删除第" << index << "个元素:" << top->data << endl;
top = top->next;
delete temp;
temp = nullptr;
index++;
}
}
template<class T>
bool stackList<T>::empty()
{
if (count == 0)
return true;
else
return false;
}
int main()
{
default_random_engine e(time(0));//产生随机数
uniform_int_distribution<int> u(0, 10000);
stackList<string> m_stackList;
for (int i = 0; i < 10; i++)
{
//+s:for test string
m_stackList.push_back(to_string(u(e))+"s");
}
for (int i = 0; i < 10; i++)
{
m_stackList.pop();
}
}
链表反转:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp; // 保存cur的下一个节点
ListNode* cur = head;
ListNode* pre = NULL;
while(cur) {
temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next
cur->next = pre; // 翻转操作
// 更新pre 和 cur指针
pre = cur;
cur = temp;
}
return pre;
}
};
5.队列
5.1 顺序循环队列
判断队列是否满:
问题:通过front和rear可以循环队列,但是却无法判断队列是否为空或满;
空队列时,front=rear;
满队列时,front=rear;
解决方式:
- 通过设置flag,当front=rear,flag=0时,为空队列;front=rear,flag=1,为满队列;
- 留出一个空位,用于区分满队列或者空队列,这样就不会出现front=rear;
通过数组实现,所以容量是一定的,判断队列状态,采用上面第二种方式:
- 由于rear可能比front大也可能比front小,也可能他们相差整整一圈:
设:队列的最大尺寸:maxsize;
那么队列满的条件就是:(rear+1)%maxsize==front; - 队列长度:
(rear-front+maxsize)%maxsize
#include<iostream>
#include <random>
using namespace std;
template<class T,std::size_t maxsize>
class mqueue
{
private:
public:
mqueue();
T arr[maxsize];
int front; //队头,用于取出数据
int rear; //队尾用于插入数据
void push_back(T data);
void pop(T &data);
int length();
void clear();//清除队列
};
template<class T,std::size_t maxsize>
mqueue<T,maxsize>::mqueue():front(0),rear(0)
{
}
template<class T,std::size_t maxsize>
void mqueue<T,maxsize>::push_back(T data)
{
if((rear+1)%maxsize==front) //队列是否满
{
cout<<"queue is full"<<endl;
return ;
}
else
{
arr[rear]=data;
rear=(rear+1)%maxsize; //rear 指针相后移动一个
return ;
}
}
template<class T,std::size_t maxsize>
void mqueue<T,maxsize>::pop(T &data)
{
if(front==rear) //当front==rear,队列为空;
{
cout<<"队列为空"<<endl;
return ;
}
else
{
data=arr[front];
front=(front+1)%maxsize;
return;
}
}
template<class T,std::size_t maxsize>
int mqueue<T,maxsize>::length()
{
return (rear-front+maxsize)%maxsize;
}
template<class T,std::size_t maxsize>
void mqueue<T,maxsize>::clear()
{
if(length()>0)
{
front=0;
rear=0;
}
}
int main()
{
default_random_engine e(time(0));
uniform_int_distribution <int> u(0,10000);
mqueue<int,200> m_queue;
for(int i=0;i<100;i++)
{
m_queue.push_back(u(e));
}
int num=0;
for(int i=0;i<50;i++)
{
m_queue.pop(num);
cout<<num<<endl;
}
return 0;
}
5.2队列的链式存储
#include <iostream>
#include <random>
using namespace std;
template<class T>
struct Node
{
T data;
Node *next;
};
template<class T>
class mqueueList
{
public:
mqueueList();
Node<T> *front, *rear, *temp;
void push_back( T data);
void pop_front( T &data);
bool empty();
int length;
//int length();
int size();
~mqueueList();
};
template<class T>
mqueueList<T>::mqueueList() : front(nullptr), rear(nullptr), temp(nullptr)
{
}
template<class T>
mqueueList<T>::~mqueueList()
{
front = nullptr;
rear = nullptr;
temp = nullptr;
}
template<class T>
void mqueueList<T>::push_back( T data)
{
length++;
if (front == nullptr)
{
rear = new Node<T>;
front = rear;
rear->data = data;
rear->next = nullptr;
temp = rear;
}
else
{
rear = new Node<T>;
rear->data = data;
rear->next = nullptr;
temp->next = rear;
temp = rear;
}
}
template<class T>
void mqueueList<T>::pop_front( T &data)
{
if (empty())
{
std::cout << "empty" << std::endl;
return;
}
else
{
Node<T> *tempPtr = front;
data = front->data;
front = front->next;
delete tempPtr;
tempPtr = nullptr;
length--;
}
}
template<class T>
bool mqueueList<T>::empty()
{
if (front == rear)
{
return true;
}
else
return false;
}
template<class T>
int mqueueList<T>::size()
{
return length;
}
int main()
{
default_random_engine e(time(0));
uniform_int_distribution<unsigned> u(0, 10000);
mqueueList<string> mqueue;
for (int i = 0; i < 10; i++)
{
mqueue.push_back(to_string(u(e))+"s");
//cout<<"length:"<<mqueue.size()<<endl;
}
Node<string> *head = mqueue.front;
while (head->next != nullptr)
{
cout << "当前数据:" << head->data << endl;
head = head->next;
}
head = mqueue.front;
while (head->next != nullptr)
{
string data;
mqueue.pop_front(data);
cout << "当前data:" << data << endl;
head = head->next;
}
}
6. 二叉树
6.1 二叉树的创建
二叉树的创建可以通过递归方式进行:
#include <iostream>
#include <stack>
#include <queue>
struct binaryTreeNode
{
char element = '#';
struct binaryTreeNode *left = nullptr;
struct binaryTreeNode *right = nullptr;
binaryTreeNode(int _element) : element(_element) {}
};
//先序创建二叉树
void createBinaryTree(binaryTreeNode * &tree)
{
char tmp;
std::cin >> tmp;
if (tmp == '#') {
tree = nullptr;
return;
}
tree = new binaryTreeNode(tmp);
createBinaryTree(tree->left);
createBinaryTree(tree->right);
return;
}
//先序遍历
void preOrder(binaryTreeNode *tree)
{
if (tree) {
std::cout << " " << tree->element;
preOrder(tree->left);
preOrder(tree->right);
}
return;
}
//先序遍历-非递归
void preOrderNonRecursive(binaryTreeNode *tree)
{
std::stack<binaryTreeNode *> sta;
binaryTreeNode *p = tree;
while (!sta.empty() || p != nullptr) {
while (p != nullptr) {
std::cout << " " << p->element;
sta.push(p);
p = p->left;
}
if (!sta.empty()) {
p = sta.top();
sta.pop();
p = p->right;
}
}
return;
}
//中序遍历
void inOrder(binaryTreeNode *tree)
{
if (tree) {
inOrder(tree->left);
std::cout << " " << tree->element;
inOrder(tree->right);
}
return;
}
//中序遍历-非递归
void inOrderNonRecursive(binaryTreeNode *tree)
{
std::stack<binaryTreeNode *> sta;
binaryTreeNode *p = tree;
while(!sta.empty() || p != nullptr) {
while (p != nullptr) {
sta.push(p);
p = p->left;
}
if (!sta.empty()) {
p = sta.top();
sta.pop();
std::cout << " " << p->element;
p = p->right;
}
}
}
//后序遍历
void postOrder(binaryTreeNode *tree)
{
if (tree) {
postOrder(tree->left);
postOrder(tree->right);
std::cout << " " << tree->element;
}
return;
}
//后序遍历-非递归
void postOrderNonRecursive(binaryTreeNode * tree)
{
std::stack<binaryTreeNode *> sta;
binaryTreeNode *cur = nullptr;
binaryTreeNode *pre = nullptr;
sta.push(tree);
while (!sta.empty()) {
cur = sta.top();
if ((cur->left == nullptr && cur->right == nullptr) ||
(pre != nullptr && (pre == cur->left || pre == cur->right))) {
//没有孩子节点或孩子节点已经被访问过了,直接输出
std::cout << " " << cur->element;
sta.pop();
pre = cur;
} else {
if (cur->right != nullptr) {
sta.push(cur->right);
}
if (cur->left != nullptr) {
sta.push(cur->left);
}
}
}
return;
}
//层次遍历
void levelOrder(binaryTreeNode *tree)
{
queue<binaryTreeNode *> que; //定义一个队列
que.push(tree); //将根元素入栈
while (!que.empty()) //当栈不为空时循环
{
binaryTreeNode *t=que.front(); //获取根节点
cout<<t->element<<endl; //输出根节点
que.pop(); //根元素出栈
if (t->left) //根元素的左孩纸存在时,入栈
{
que.push(t->left);
}
if (t->right) //根元素的右孩纸存在时,入栈
{
que.push(t->right);
}
}
}
int main(void)
{
//a b d # g # # # c e # # f h # # #
std::cout << "请输入先序序列用于构建二叉树:" << std::endl;
binaryTreeNode *tree = nullptr;
createBinaryTree(tree);
std::cout << "先序遍历结果: ";
preOrder(tree);
std::cout << std::endl;
std::cout << "先序遍历结果(非递归):";
preOrderNonRecursive(tree);
std::cout << std::endl;
std::cout << "中序遍历结果: ";
inOrder(tree);
std::cout << std::endl;
std::cout << "中序遍历结果(非递归):";
inOrderNonRecursive(tree);
std::cout << std::endl;
std::cout << "后序遍历结果: ";
postOrder(tree);
std::cout << std::endl;
std::cout << "后序遍历结果(非递归):";
postOrderNonRecursive(tree);
std::cout << std::endl;
std::cout << "层次遍历结果(非递归):";
levelOrder(tree);
std::cout << std::endl;
return 0;
}
已知先序与中序序列求该二叉树:
先序遍历为:A B C D E F G H I J
中序遍历为:C D B F E A I H G J
先序遍历:根->左->右,中序遍历:左->根->右,所以我们只需找到根,那么就可以根据中序遍历,找出此根的左子树与右子树中序遍历中,根左侧的元素,就是该根元素的左子树,根右侧的元素,就是该根元素的右子树;然后在其左子树与右子树在找到根,然后在根据中序遍历,找到其左子树与右子树以此类推;
- 从先序遍历中,可知根节点A;
- 已知根节点A,则右中序遍历可知,A的左子树为:C D B F E ,A的右子树为:I H G J
- 先看A的左子树为:C D B F E
- 在先序遍历中排列为:B C D E F,所以根节点为B;
- 再根据此子树的中序,可知该子树的左子树为:C D ,右子树为:FE;然后在看此子树的左子树:
- 先序遍历:为C D,可知C为根节点;
- 根据中序:D在C的右边,所以D为C的右子树;
- 以此类推。
其方式和已知前序相同,首要目标找到根节点;
7.图
7.1 无向图邻接矩阵

[ ∞ 10 50 40 10 ∞ 20 ∞ 50 20 ∞ 30 40 8 30 ∞ ] \begin{bmatrix} \infty & 10 & 50 & 40\\ 10 & \infty & 20 & \infty \\ 50 & 20 & \infty &30 \\ 40 & 8 & 30 & \infty \end{bmatrix} ⎣⎢⎢⎡∞10504010∞2085020∞3040∞30∞⎦⎥⎥⎤
//---------------------------------------------------------------------
#include <iostream>
#include <string>
#include <limits.h>
using namespace std;
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100 //定义最大顶点数;
#define INFINITY INT_MAX //代表无穷大
struct MGraph
{
VertexType vexs[MAXVEX]; //定义一个顶点表;
EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵,可以看做边表;
int currVex, currEdges; //当前的顶点数与边数;
void PrintWeights()
{
for (int i = 0; i < currVex; i++)
{
for (int j = 0; j < currVex; j++)
{
cout<<arc[i][j]<<"\t";
}
cout<<endl;
}
}
void PrintVexs()
{
for(int i=0;i<currVex;i++)
{
cout<<vexs[i]<<"\t";
}
cout<<endl;
}
};
int LocatVex(MGraph &G, VertexType &vex)
{
int i = 0;
for (i; i < G.currVex; i++)
{
if (vex == G.vexs[i])
return i;
}
return -1;
}
void CreatMGraph(MGraph &G)
{
cout << "输入顶点数与边数" << endl;
cin >> G.currVex >> G.currEdges;
cout << "创建顶点表" << endl;
for (int i = 0; i < G.currVex; i++)
{
static bool flag=false;
if(!flag)
{
cout << "输入顶点..." << endl;
flag=true;
}
cin >> G.vexs[i];
}
cout << "初始化邻接矩阵..." << endl;
for (int i = 0; i < G.currVex; i++)
{
for (int j = 0; j < G.currVex; j++)
{
G.arc[i][j] = INFINITY;
}
}
cout << "创建邻接矩阵..." << endl;
for (int k = 0; k < G.currEdges; k++)
{
cout << "邻接矩阵中顶点元素v1,v2,以及权重w" << endl;
VertexType v1, v2;
int w; //权值
cin >> v1 >> v2 >> w;
int i = LocatVex(G, v1);
int j = LocatVex(G, v2);
G.arc[i][j] = w;
G.arc[j][i] = G.arc[i][j];
}
cout << "邻接矩阵创建完成..." << endl;
return;
}
int main()
{
MGraph m_graph;
CreatMGraph(m_graph);
m_graph.PrintWeights();
m_graph.PrintVexs();
return 0;
}
//输入顶点数与边数
4 5
创建顶点表
输入顶点...
a b c d
初始化邻接矩阵...
创建邻接矩阵...
邻接矩阵中顶点元素v1,v2,以及权重w
a b 10
邻接矩阵中顶点元素v1,v2,以及权重w
a c 50
邻接矩阵中顶点元素v1,v2,以及权重w
a d 40
邻接矩阵中顶点元素v1,v2,以及权重w
b c 20
邻接矩阵中顶点元素v1,v2,以及权重w
c d 30
邻接矩阵创建完成...---------------------------------------------------------------------
#include <iostream>
using namespace std;
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define INFINTY 65535
struct MGraph
{
VertexType vexs[MAXVEX]; //顶点表
EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵
int numVertexs, numEdges; //图中当前的顶点数和边数
};
void CreatMGraph(MGraph *G)
{
int i, j, k, w;
cout << "输入顶点数和边数:" << endl;
cin >> G->numVertexs >> G->numEdges;
//读入顶点信息,建立顶点表;
for (int i = 0; i < G->numVertexs; i++)
{
//依次输入顶点内容;
static bool ok = false;
if (!ok)
{
cout << "输入顶点内容" << endl;
ok = true;
}
cin >> G->vexs[i];
}
for (i = 0; i < G->numVertexs; i++)
{
for (j = 0; j < G->numVertexs; j++)
{
G->arc[i][j] = INFINTY; //初始化为最大值
}
}
for (k = 0; k < G->numEdges; k++)
{
cout << "输入边(vi,vj)的上下标,i,j和权重:w" << endl;
cin >> i >> j >> w;
G->arc[i][j] = w;
G->arc[j][i] = G->arc[i][j]; //因为是无向图,所以图应该是对称矩阵;
}
}
int main()
{
MGraph mGraph;
CreatMGraph(&mGraph);
return 0;
}
7.2 邻接表
邻接矩阵的缺点:对于边数相对顶点较少的图,对存储空间比较浪费;
#include <iostream>
using namespace std;
typedef char VertexType; //节点数据类型
typedef int EdgeType; //边上权值类型
#define MAXVEX 100 //定义最大顶点数;
#define INFINITY INT_MAX //代表无穷大
struct EdgeNode //边表节点
{
int adjvex; //邻接点域,存储该节点对应的下标;
EdgeType weight; //权值
EdgeNode *next; //指向下一个邻接点
};
struct VertexNode //顶点表节点
{
VertexType data; //顶点域,存储顶点数据;
EdgeNode *firstedge; //边表头指针,指向第一个邻接点;
};
struct GraphAdList
{
VertexNode adjList[MAXVEX]; //一个VertexNode数组,存贮节点
int currVexnums, currEdgenums; //当前顶点数与边数;
};
int LocateVex(GraphAdList &G, VertexType &vex)
{
int i = 0;
for (i; i < G.currVexnums; i++)
{
if (vex == G.adjList[i].data)
{
return i;
}
}
return -1;
}
void PrintAdListItem(GraphAdList &G)
{
for (int i = 0; i < G.currVexnums; i++)
{
cout << G.adjList[i].data << "\t";
EdgeNode *head = G.adjList[i].firstedge;
while (true)
{
cout << G.adjList[head->adjvex].data << "\t";
head = head->next;
if (head->next == nullptr)
{
cout << G.adjList[head->adjvex].data << "\t";
break;
}
}
cout << endl;
}
}
void CreatALGraph(GraphAdList &G)
{
cout << "请输入顶点数与边数" << endl;
cin >> G.currVexnums >> G.currEdgenums;
if ((G.currVexnums == 0) || (G.currEdgenums == 0))
{
return;
}
cout << "请输入顶点" << endl;
for (int i = 0; i < G.currVexnums; i++)
{
cin >> G.adjList[i].data;
G.adjList[i].firstedge = nullptr;
}
EdgeNode *e = nullptr; //用于创建子链表
for (int k = 0; k < G.currEdgenums; k++)
{
cout << "请依次输入一条边上依附的顶点" << endl;
VertexType v1, v2;
cin >> v1 >> v2;
int i = LocateVex(G, v1);
int j = LocateVex(G, v2);
e = new EdgeNode;
e->adjvex = j;
e->next = G.adjList[i].firstedge; /*头插法*/
G.adjList[i].firstedge = e;
e = new EdgeNode;
e->adjvex = i;
e->next = G.adjList[j].firstedge; /*头插法,如果是有向图,则创建一次就行*/
G.adjList[j].firstedge = e;
}
}
int main()
{
GraphAdList m_graphlist;
CreatALGraph(m_graphlist);
PrintAdListItem(m_graphlist);
}
- 对于有向图,如果我们只关心
出度
即采用邻接表
的方法,那么如果想知道一个节点的入度
,则需要遍历整个邻接表; - 如果我们只关心
入度
即采用逆邻接表
的方法,那么如果想知道一个节点的出度度
,则需要遍历整个邻接表;
==>解决方法:十字链表;
7.3 图的遍历
此部分概念参考:https://developer.51cto.com/art/202004/614590.htm
7.3.1 深度优先遍历
深度优先遍历(Depth First Search, 简称 DFS) 与广度优先遍历(Breath First Search)是图论中两种非常重要的算法,生产上广泛用于拓扑排序,寻路(走迷宫),搜索引擎,爬虫等,也频繁出现在 leetcode,高频面试题中。
- 深度优先遍历

主要思路是从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底…,不断递归重复此过程,直到所有的顶点都遍历完成,它的特点是不撞南墙不回头,先走完一条路,再换一条路继续走。
那么深度优先遍历该怎么实现呢,有递归和非递归两种表现形式,接下来我们以二叉树为例来看下如何分别用递归和非递归来实现深度优先遍历。
- 递归实现
//邻接矩阵
#include<iostream>
#include<limits.h>
using namespace std;
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100 //定义最大顶点数;
#define INFINITY INT_MAX //代表无穷大
bool visited[MAXVEX];//访问标志位
struct MGraph
{
VertexType vexs[MAXVEX]; //定义一个顶点表;
EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵,可以看做边表;
int currVex, currEdges; //当前的顶点数与边数;
void PrintWeights()
{
for (int i = 0; i < currVex; i++)
{
for (int j = 0; j < currVex; j++)
{
cout<<arc[i][j]<<"\t";
}
cout<<endl;
}
}
void PrintVexs()
{
for(int i=0;i<currVex;i++)
{
cout<<vexs[i]<<"\t";
}
cout<<endl;
}
};
int LocatVex(MGraph &G, VertexType &vex)
{
int i = 0;
for (i; i < G.currVex; i++)
{
if (vex == G.vexs[i])
return i;
}
return -1;
}
void CreatMGraph(MGraph &G)
{
cout << "输入顶点数与边数" << endl;
cin >> G.currVex >> G.currEdges;
cout << "创建顶点表" << endl;
for (int i = 0; i < G.currVex; i++)
{
static bool flag=false;
if(!flag)
{
cout << "输入顶点..." << endl;
flag=true;
}
cin >> G.vexs[i];
}
cout << "初始化邻接矩阵..." << endl;
for (int i = 0; i < G.currVex; i++)
{
for (int j = 0; j < G.currVex; j++)
{
G.arc[i][j] = INFINITY;
}
}
cout << "创建邻接矩阵..." << endl;
for (int k = 0; k < G.currEdges; k++)
{
cout << "邻接矩阵中顶点元素v1,v2,以及权重w" << endl;
VertexType v1, v2;
int w; //权值
cin >> v1 >> v2 >> w;
int i = LocatVex(G, v1);
int j = LocatVex(G, v2);
G.arc[i][j] = w;
G.arc[j][i] = G.arc[i][j];
}
cout << "邻接矩阵创建完成..." << endl;
return;
}
//------------------------------------------------------------
void DFS(MGraph &G,int i);
void DFSTraverse(MGraph &G)
{
int i;
for(i=0;i<G.currVex;i++)
{
visited[i]=false;//标志位数组
}
for(i=0;i<G.currVex;i++)
{
if(!visited[i])//查看标志位
{
DFS(G,i);
}
}
}
void DFS(MGraph &G,int i)
{
int j;
cout<<G.vexs[i]<<"==>";//输出顶点
visited[i]=true;
for(j=0;j<G.currVex;j++)
{
if(G.arc[i][j]!=INFINITY&&!visited[j])
DFS(G,j);
}
cout<<endl;
}
//-------------------------------------------------------------
int main()
{
MGraph m_graph;
CreatMGraph(m_graph);
DFSTraverse(m_graph);
return 0;
}
7.3.2 广度优先遍历

8. 查找
查找表:是一些由同一类型的数据元素(或记录)构成的集合;
关键字:是数据中某个数据项的值,又称为键值;
主关键字:可以唯一标示一个记录;
次关键字:可以识别多个数据元素的关键字;
静态查找表:只做查找操作的查找表;
动态查找表:在查找过程中,同时插入不存在的元素,或从查找表中删除已经存在的元素;
8.1 顺序查找算法
#include<iostream>
using namespace std;
/* 顺序查找
* @param:a为数组;
* @param:n 要查找的数组长度;
* @param:key 要查找的关键字;
*/
int Sequential_Search(int *a,int n,int key)
{
int i;
for(i=1;i<n;i++) // 1.进行越界检查
{
if(key==a[i]) // 2.进行查询
return i;
}
return 0;
}
int main()
{
int array[100];
for(int i=0;i<100;i++)
{
array[i]=i;
}
int index= Sequential_Search(array,100,50);
cout<<index<<endl;
return 0;
}
由上面程序可知,程序每次循环都需要进行两次判断
- 越界判断,即
i<n
; - 比较
key==a[i]
改进:==>O(n)
#include<iostream>
using namespace std;
/* 顺序查找
* @param:a为数组;
* @param:n 要查找的数组长度;
* @param:key 要查找的关键字;
*/
int Sequential_Search_Improve(int *a,int n,int key)
{
int i;
a[0]=key; //设置哨兵,且从1位置开始存储数据;
i=n; //从尾部遍历,所以也有缺点;
while(key!=a[i]) //只进行一次判断;
{
i--;
}
return i; //返回0说明查找失败;
}
int main()
{
int array[100];
for(int i=0;i<100;i++)
{
array[i]=i;
}
int index= Sequential_Search_Improve(array,100,50);
cout<<index<<endl;
return 0;
}
8.2 有序查找
8.2.1 折半查找
使用折半查找的前提必须是有序数列;
#include<iostream>
using namespace std;
/* 顺序查找
* @param:a为数组;
* @param:n 要查找的数组长度;
* @param:key 要查找的关键字;
*/
int Binary_Search(int *a,int n,int key)
{
int low,high,middle;
low=1;
high=n;
while (low<=high) //此处是小于等于
{
middle=(low+high)/2;
if(key<a[middle])
{
high=middle-1;
}
else if(key>a[middle])
{
low=middle+1;
}
else {
return middle; //查找成功;
}
}
return 0;
}
int main()
{
int array[100];
for(int i=0;i<100;i++)
{
array[i]=i*2;
}
for(auto &index:array)
{
cout<<index<<endl;
}
int index= Binary_Search(array,100,16);
cout<<index<<endl;
return 0;
}
8.2.2 插值查找
- 折半查找也是有弊端的,假若从0-1000000中查找数据5,如果采用折半查找就不是最优算法;
- 为什么非要一分为二进行查找?
为了解决上面两种问题,对折半查找进行改进:
在折半查找中,通常需要计算:
m i d = l o w + h i g h 2 mid=\frac{low+high}{2} mid=2low+high
进一步进行变形:
m i d = l o w + h i g h 2 = l o w + h i g h − l o w 2 mid=\frac{low+high}{2} =low+\frac{high-low}{2} mid=2low+high=low+2high−low
折半查找的效率主要在因为1/2,所以查找效率主要取决这个参数:可以将这个参数进行优化:
m i d = l o w + k e y − a [ l o w ] a [ h i g h ] − a [ l o w ] ( h i g h − l o w ) mid=low+\frac{key-a[low]}{a[high]-a[low]} (high-low) mid=low+a[high]−a[low]key−a[low](high−low)
#include<iostream>
using namespace std;
/* 顺序查找
* @param:a为数组;
* @param:n 要查找的数组长度;
* @param:key 要查找的关键字;
*/
int Interpolation_Search(int *a,int n,int key)
{
int low,high,middle;
low=1;
high=n;
while (low<=high) //此处是小于等于
{
middle=low+(high-low)*(key-a[low])/(a[high]-a[low]);
if(key<a[middle])
{
high=middle-1;
}
else if(key>a[middle])
{
low=middle+1;
}
else {
return middle; //查找成功;
}
}
return 0;
}
int main()
{
int array[100];
for(int i=0;i<100;i++)
{
array[i]=i*2;
}
// for(auto &index:array)
// {
// cout<<index<<endl;
// }
int index= Interpolation_Search(array,100,180);
cout<<index<<endl;
return 0;
}
8.2.2 斐波那契查找
斐波那契数列,又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、····,在数学上,斐波那契被递归方法如下定义:F(1)=1,F(2)=1,F(n)=f(n-1)+F(n-2) (n>=2)。该数列越往后相邻的两个数的比值越趋向于黄金比例值(0.618)。
斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的。在斐波那契数列找一个等于略大于查找表中元素个数的数F[n],将原查找表扩展为长度为F[n](如果要补充元素,则补充重复最后一个元素,直到满足F[n]个元素),完成后进行斐波那契分割,即F[n]个元素分割为前半部分F[n-1]个元素,后半部分F[n-2]个元素,找出要查找的元素在那一部分并递归,直到找到。
比如有一个数组arr={1,2,3,4,5,6,7,8,9,10,11,12}要对他进行斐波那契查找,查找的值是10
首先,你得创建一个斐波那契数列出来我们定义为f[k],长度暂且定义为10吧那f={1,1,2,3,5,8,13,21,34,55},创建好了之后,我们再看原数组长度arr.length=12,根据斐波那契查找原则我们发现他的长度不等于斐波那契数列的某一个数值,所以我们要将数组的长度补至最近的斐波那契数,好,我们最近的值是13,所以我们复制最后一个元素至arr数组末尾(当然,数组长度是不能改变的,我们只能创建一个新的数组来复制arr数组的值并复制最后一个元素添加到末尾),好,新的数组元素就是{1,2,3,4,5,6,7,8,9,10,11,12,12}
斐波那契查找的时间复杂度还是O(log 2 n ),但是 与折半查找相比,斐波那契查找的优点是它只涉及加法和减法运算,而不用除法,而除法比加减法要占用更多的时间,因此,斐波那契查找的运行时间理论上比折半查找小,但是还是得视具体情况而定。
- 所以无论是插值查找还是斐波那契查找,都是对二分查找的优化,选择合适的分割手段,提高查找效率
#include<iostream>
using namespace std;
/* 顺序查找
* @param:a为数组;
* @param:n 要查找的数组长度;
* @param:key 要查找的关键字;
*/
int Create_Fibonacci(int n)
{
if(n<2)
return n==0?0:1;
return Create_Fibonacci(n-1)+ Create_Fibonacci(n-2);
}
int Fibonacci_Search(int *a,int n,int key)
{
/*创建斐波那契查询数组F*/
//创建斐波那契查询数组F,比较费时,可以提前创建好;
int F[25];
for(int i=0;i<25;i++)
{
F[i]=Create_Fibonacci(i);
}
int low,high,middle,i,k;
low=1;
high=n;
k=0;
while (n>F[k]-1) //计算查询数组所需长度;
{
k++;
}
for(i=n;i<F[k]-1;i++)
a[i]=a[n]; //此处需要保证数组长度是否越界;
while(low<=high)
{
middle=low+F[k-1]-1;
if(key<a[middle])
{
high=middle-1;
k=k-1;
}
else if (key>a[middle]) {
low=middle+1;
k=k-2;//注意是k-2
}
else {
if(middle<=n) //判断是否越界
return middle;
else {
return n; //middle>n,说明是补全数值,返回n;
}
}
}
return -1;
}
int main()
{
int array[200]={0};
for(int i=0;i<100;i++)
{
array[i]=i*2;
}
cout<<endl;
cout<< Fibonacci_Search(array,100,42)<<endl;
return 0;
}
//此代码存在BUG,输入180==>-1?
8.2 线性索引查找
无论是二分查找
插值查找
或者是斐波那契查找
均需要在有序的基础上进行;对于数据量较大数据集,
索引
:就是将一个关键字与它对应的记录相关联的过程;
线性索引
:将索引项集合组织为线性结构;
8.2.1 稠密索引
稠密索引是指:在线性索引中,将数据集的每一个记录对应一个索引项;

对于稠密索引这个索引表来说,索引项一定是按照关键码有序排列的;
索引项有序,所以查找关键字时,可以用折半,插值,斐波
那契等有序查找算法;
8.2.2 分块索引
稠密索引因为索引项与数据集的记录个数相同,所以空间代价很大.为了减少索引项的个数,可以对数据集进行分块,使得分块有序,然后在对每一块建立一个索引项;
分块索引有以下两点:
- 块间有序:例如,要求第二块所有记录的关键字均要大于第一块所有记录的关键字,同样,第三块的所有记录的关键字均要大于第二块的所有记录关键字;
- 块内无序:即每一块内的记录不要求有序;
分块索引的索引项结构分三个数据项:
最大关键码
,它存储每一块中的最大关键字,这样的好处就是可以使得在它之后的下一块中最小一个关键字
也能比这一块最大的关键字要大
;- 存储块中的存储的
记录个数
; - 用于指向块首元素的
第一个指针
;

8.2.3 倒序索引
8.3 二叉排序树
8.3.1 二叉排序树-查找
步骤:若根结点的关键字值等于查找的关键字,成功。
否则,若小于根结点的关键字值,递归查左子树。
若大于根结点的关键字值,递归查右子树。
若子树为空,查找不成功。
8.3.2 二叉排序树-插入
首先执行查找算法,找出被插结点的父亲结点。
判断被插结点是其父亲结点的左、右儿子。将被插结点作为叶子结点插入。
若二叉树为空。则首先单独生成根结点。
注意:新插入的结点总是叶子结点。
8.3.3 二叉排序树-删除
在二叉排序树删去一个结点,分三种情况讨论:
- 若*p结点为叶子结点,即PL(左子树)和PR(右子树)均为空树。由于删去叶子结点不破坏整棵树的结构,则可以直接删除此子结点。
- 若p结点只有左子树PL或右子树PR,此时只要令PL或PR直接成为其双亲结点f的左子树(当p是左子树)或右子树(当p是右子树)即可,作此修改也不破坏二叉排序树的特性。
- 若*p结点的左子树和右子树均不空。通常用一下两种方法解决:
- 寻找左子树的最大值,将此最大值赋值与待删除的节点,删除左子树的最大值;
- 寻找右子树的最小值,将此最小值赋值与待删除的节点,删除左子树的最小值;
为什么要这样做?因为这样做可以找到与待删除节点最为接近的值,用这个最接近的值替换掉待删除的节点,才能使得变化最小;
- 二叉排序树,删除操作主要针对三种情况。
-
叶子节点-直接删除就可以了
-
没有左孩子的节点-直接嫁接右子树就可以了(没有右孩子的节点-直接嫁接左子树就可以了)
-
如果左右子树都存在,则寻找删除节点的直接前驱(即左子树里面的最右的节点)
#include<iostream>
using namespace std;
typedef int status;
//定义一个树的结构体
typedef struct BiTNode
{
int data;
struct BiTNode* lchild, * rchild;
}BiTNode, * BiTree;
//函数声明
void CreateBST(BiTree* T, int a[], int n);
void outputBST(BiTree T);
status InsertBST(BiTree* T, int key);
status DeleteBST(BiTree*T,int key);
status Delete(BiTree *p);
//二叉排序树的查找函数定义
status SearchBST(BiTree T,int key,BiTree f,BiTree *p)
{
if (!T)
{
*p = f;
return false;
}
else if (key==T->data)
{
*p = T;
return true;
}
else if (key<T->data)
{
return SearchBST(T->lchild,key,T,p);
}
else
{
return SearchBST(T->rchild,key,T,p);
}
}
//二叉排序树的插入函数定义
status InsertBST(BiTree *T,int key)
{
BiTree p=NULL, s=NULL;
if (!SearchBST(*T,key,NULL,&p))
{
s = (BiTree)malloc(sizeof(BiTNode));
s->data = key;
s->lchild = s->rchild = NULL;
if (!p)
* T = s;
else if (key < p->data)
p->lchild = s;
else
p->rchild = s;
return true;
}
return false;
}
//二叉排序树的删除操作函数定义
status DeleteBST(BiTree* T, int key)
{
if (!*T)
return false;
else
{
if (key == (*T)->data)
{
return Delete(T);
}
else if (key<(*T)->data)
{
return DeleteBST(&(*T)->lchild,key);
}
else
{
return DeleteBST(&(*T)->rchild,key);
}
}
}
//根据节点的三种情况来删除节点
status Delete(BiTree* p)
{
BiTree q, s;
if ((*p)->rchild==NULL)
{
q = *p; *p = (*p)->lchild; free(q);
}
else if ((*p)->lchild==NULL)
{
q = *p; *p = (*p)->rchild; free(q);
}
else
{
q = *p; s = (*p)->lchild;
while (s->rchild)
{
q = s; s = s->rchild;
}
(*p)->data = s->data;
if (q != *p)
q->rchild = s->lchild;
else
q->lchild = s->lchild;
free(s);
}
return true;
}
//通过一个数组来创建二叉排序树
void CreateBST(BiTree*T, int a[], int n)
{
int i;
for (i = 0; i < n; i++)
{
InsertBST(T, a[i]);
}
}
//把一个二叉排序树中序遍历打印
void outputBST(BiTree T)
{
if (T == NULL)
{
return;
}
outputBST(T->lchild);
cout << T->data << " ";
outputBST(T->rchild);
}
//主函数
int main()
{
int a[] = { 62,88,58,47,35,73,51,99,37,93 };
BiTree T = NULL;
//创建二叉排序树
CreateBST(&T, a, 10);
//在二叉排序树中插入95
InsertBST(&T, 95);
//在二叉排序树中查找节点
int b = 95;
BiTree p = NULL;
if (!SearchBST(T, b, NULL, &p))
cout << "没有找到" << endl;
else
{
cout << b << "查找结果的指针为:\n" << p << endl;
}
//在二叉排序树中删除88节点
DeleteBST(&T, 88);
//验证上述的插入和删除操作
outputBST(T);
cout << endl;
return 0;
}
//--------------------------------------------
struct BiTree {
int data;
BiTree *lchild;
BiTree *rchild;
};
//在二叉排序树中插入查找关键字key
BiTree* InsertBST(BiTree *t,int key)
{
if (t == nullptr) //检查当前节点是否为空
{
t = new BiTree();
t->lchild = t->rchild = nullptr;
t->data = key;
return t;
}
if (key < t->data)
t->lchild = InsertBST(t->lchild, key);
else
t->rchild = InsertBST(t->rchild, key);
return t;
}
//n个数据在数组d中,tree为二叉排序树根
BiTree* CreateBiTree(BiTree *tree, int d[], int n)
{
for (int i = 0; i < n; i++)
tree = InsertBST(tree, d[i]);
return tree;
}
8.4 散列表查找
散列技术是在存储位置和它的关键字之间建立一个确定的函数关系f,使得没一个关键字key对应一个存储位置f(key);我们称这个f为散列函数或者是哈希(Hash)函数,采用散列技术将记录存储在一块连续的内存存储空间中,这块连续的存储空间称为散列表或哈希表;
即,在存储时通过一个函数关系根据关键字计算出来存储位置,然后进行存储,在查找时,同样通过该函数直接找到其存储位置;
8.4.1 散列表查找步骤
- 在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录;
- 在查找时,我们通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录.
- 冲突
在理想情况下,每一个关键字,通过散列函数计算出来的地址是不一样的,可现实中,我们经常会碰到两个关键字key1≠key2,但是却f(key1)=f(key2),此现象称为冲突,并把key1与key2称为这个散列函数的同义词;
8.4.2 散列函数构造方法
-
直接地址法:
指定一个关键字函数,用于存储与查找;
例如:
1. f(key)=key;
2. f(key)=a*key+b;(a,b为常数)直接地址法:这样的散列函数优点就是简单,均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况,不常用;
-
数字分析法:
通过分析存储数据特征提取关键字,作为地址进行存储;
例如:手机尾号;
数字分析法:通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑; -
平方取中法:
假如:关键字为1234==>平方==>1522756==>抽取中间三位==>227;
关键字:4321==>18671041==>671/710;
平方取中法:适合不知道关键字的分布,而位数不是很大的情况; -
折叠法:
将关键字从左到右进行分割成位数相等的几部分(最后一部分可以短一点),然后将这几部分叠加求和,并按散列表表长,取最后几位作为散列地址;
例如:9876543210==>987|654|321|0==>求和==>987+654+321+0=1962,在取后面3位得到三位作为散列地址==>962;
折叠法:事先可以不需要知道关键字的分布,适合关键字位数较多的情况; -
除留余数法:(常用)
f(key)=key mod p(p≤m);//mod 取余数;
除留余数法关键是在p的选取上,所以一般情况下p的取值为:
若散列表表长为m,通常p小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数;
6. 随机数法:
选择一个随机数,取关键字的随机数值为它的散列地址==>f(key)=random (key);
随机数法:关键字长度不等时;
8.4.3 解决冲突
- 开放地址法:
开放地址法:就是一旦发生冲突就去寻找下一个空的散列表地址,只要散列表足够大,空的散列地址总能找到,并将记录存入;
fi(key)=[f(key)+di] mod m (di=1,2,3…);
若增加的di是随机生成的,则称为==>随机探测法; - 公共溢出法:
此时需要多个散列表,当产生冲突时,将冲突的数据存储新的一个散列表上对应位置;
查找时,先查找基本表,在查找溢出表; - 再散列函数法:
发生冲突时,更改散列函数,重新存贮;
#include<iostream>
using namespace std;
#define HASHSIZE 12
#define NULLKEY -327628
struct HashTable
{
int *elment;//指向数组的头指针
//动态分配数组
int count;//当前数组个数
};
int m=12;//散列表长度;
bool InitHashTable(HashTable *H)
{
int i=0;
m=HASHSIZE;
H->count=m;
H->elment=new int [m];
for(i;i<m;i++)
{
H->elment[i]=NULLKEY;
}
return true;
}
/*散装函数*/
int Hash(int key)
{
return key %m;//除留余数法
}
void InsertHash(HashTable *H,int key)
{
int addr=Hash(key);
while (H->elment[addr]!=NULLKEY)//判断是否冲突
{
addr=(addr+1)%m;//开放地址法的线性探测;
}
H->elment[addr]=key;
}
bool SearchHash(HashTable *H,int key)
{
int addr=Hash(key);
while(H->elment[addr]!=key)
{
addr=(addr+1)%m;//线性探测
if(H->elment[addr]==NULLKEY||addr==Hash(key))//为空或者从原点开始;
{
return false;
}
}
return true;
}
int main()
{
HashTable hashtable;
InitHashTable(&hashtable);
int data[12]={12,67,56,16,25,37,22,29,15,47,48,34};
for(int i=0;i<m;i++)
{
InsertHash(&hashtable,data[i]);
}
//测试查找
cout<<boolalpha<<SearchHash(&hashtable,98)<<endl;
return 0;
}
9.排序
- 稳定排序不稳定排序:序列中若有相同元素,排序前后顺序不发生改变的是稳定排序,否则是不稳定排序;
- 常见不稳定排序:
- 堆排序
- 希尔排序
- 快速排序
- 选择排序
一句话:一堆
希尔
快
选择

9.1 插入排序
9.1.1 直接插入排序
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
- 算法步骤
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。) - 动图演示
#include<iostream>
#include <random>
using namespace std;
#define MAXSIZE 10 //用于要排序的数组的最大个数
struct SqList{
int r[MAXSIZE+1];//用于存储要排序的数组;r[0]用于哨兵或临时变量;
int length;//顺序表的长度;
};
/*直接插入排序*/
//查找是使用顺序查找法查找要插入的位置:
void insertion_sort(int arr[],int len){
for(int i=1;i<len;i++){
int key=arr[i];
int j=i-1;
while((j>=0) && (key<arr[j]))//两次比较;
{
arr[j+1]=arr[j];
j--;
}
arr[j+1]=key;
}
}
/*顺序插入排序改进版*/
void InsertSort(SqList *L)
{
int i=0,j=0;
//依次插入第2-n个元素
for(i=2;i<=L->length;i++)//i=2从数据第二个位置开始比较,是由于L->r[0]作为哨兵,L->r[1]是第一个数据;
{
if (L->r[i]<L->r[i-1]) /* 需将L->r[i]插入有序子表 */
{
L->r[0]=L->r[i]; /* 设置哨兵 */
for(j=i-1;L->r[j]>L->r[0];j--)
L->r[j+1]=L->r[j]; /* 记录后移 */
L->r[j+1]=L->r[0]; /* 插入到正确位置 */
}
}
}
/*折半插入排序*/
//查找是使用折半查找法查找要插入的位置:
int main()
{
SqList listdata;
listdata.length=10;
default_random_engine e(time(0));
uniform_int_distribution<int>u(0,500);
for(int i=1;i<=10;i++)
{
listdata.r[i]=u(e);
}
for(auto &index:listdata.r)
{
cout<<index<<endl;
}
cout<<endl;
InsertSort(&listdata);
for(auto &index:listdata.r)
{
//第一个输出的是哨兵值,所以出现了逆序;
cout<<index<<endl;
}
return 0;
}
9.1.2 折半插入排序
- 基本概念
折半插入排序(binary insertion sort)是对插入排序算法的一种改进,由于排序算法过程中,就是不断的依次将元素插入前面已排好序的序列中。由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。
- 算法思想
在将一个新元素插入已排好序的数组的过程中,寻找插入点时,将待插入区域的首元素设置为a[low],末元素设置为a[high],则轮比较时将待插入元素与a[mid],其中m=(low+high)/2相比较,如果比参考元素大,则选择a[low]到a[mid-1]为新的插入区域(即high=m-1),否则选择a[mid+1]到a[high]为新的插入区域(即low=m+1),如此直至low<=high不成立,即将此位置之后所有元素后移一位,并将新元素插入a[high+1]。
- 稳定性及复杂度
折半插入排序算法是一种稳定的排序算法,比直接插入算法明显减少了关键字之间比较的次数,因此速度比直接插入排序算法快,但记录移动的次数没有变,所以折半插入排序算法的时间复杂度仍然为O(n^2),与直接插入排序算法相同。附加空间O(1)。
#include<iostream>
#include <random>
using namespace std;
#define MAXSIZE 10 //用于要排序的数组的最大个数
struct SqList{
int r[MAXSIZE+1];//用于存储要排序的数组;r[0]用于哨兵或临时变量;
int length;//顺序表的长度;
};
void BinsertSort(SqList *L)
{
int i=0,j=0;
int low=0,high=0,mid=0;
for(i=2;i<=L->length;i++)//依次插入第2-n个元素
{
L->r[0]=L->r[i];//将当前插入元素存放到"哨兵"位置
low=1;
high=i-1;
while (low<=high)
{
mid=(high+low)/2;
if(L->r[mid]<L->r[0])//此处应该比较的是mid;
low=mid+1; //因为要插入的值比中间值大,所以在右半段
else {
high=mid-1;
}
}//循环结束后,high+1就是要插入的位置
for(j=i-1;j>=high+1;j--)
{
L->r[j+1]=L->r[j];//向后移动数据
}
L->r[high+1]=L->r[0];//插入元素
}
}
int main()
{
SqList listdata;
listdata.length=10;
default_random_engine e(time(0));
uniform_int_distribution<int>u(0,500);
for(int i=1;i<=10;i++)
{
listdata.r[i]=u(e);
}
for(auto &index:listdata.r)
{
cout<<index<<endl;
}
cout<<endl;
BinsertSort(&listdata);
for(auto &index:listdata.r)
{
//第一个输出的是哨兵值,所以出现了逆序;
cout<<index<<endl;
}
return 0;
}
9.1.3 希尔排序
#include<iostream>
#include <random>
using namespace std;
#define MAXSIZE 10 //用于要排序的数组的最大个数
struct SqList{
int r[MAXSIZE+1];//用于存储要排序的数组;r[0]用于哨兵或临时变量;
int length;//顺序表的长度;
};
/*希尔排序*/
/*
* 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,
* 每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
*/
void ShellInsert(SqList *L,int dk)
{
//对顺序表L进行一趟增量为dk的shell排序,dk为步长因子
int i=0,j=0;
for(i=dk+1;i<=L->length;i++)
{
//进行增量dk依次减少的插入排序==>i±1==>i±dk
if(L->r[i]<L->r[i-dk])
{
L->r[0]=L->r[i];
for(j=i-dk;j>0&&(L->r[0]<L->r[j]);j=j-dk)
{
L->r[j+dk]=L->r[j];
}
L->r[j+dk]=L->r[0];
}
}
}
/*
* @param:dlta存储希尔排序增量的的数组;注意最后一个增量必须是1;
* @param:t增量个数;
*/
void ShellSort(SqList *L,int *dlta,int t)
{
//按增量序列依次对顺序表进行排列
for(int i=0;i<t;i++)
ShellInsert(L,dlta[i]);
}
int main()
{
SqList listdata;
listdata.length=10;
default_random_engine e(time(0));
uniform_int_distribution<int>u(0,500);
for(int i=1;i<=10;i++)
{
listdata.r[i]=u(e);
}
for(auto &index:listdata.r)
{
cout<<index<<endl;
}
cout<<endl;
int dlta[3]={5,3,1};//增量数组
ShellSort(&listdata,dlta,3);
for(auto &index:listdata.r)
{
//第一个输出的是哨兵值,所以出现了逆序;
cout<<index<<endl;
}
return 0;
}
9.2 交换排序
9.2.1 冒泡排序
#include<iostream>
#include <random>
using namespace std;
#define MAXSIZE 10 //用于要排序的数组的最大个数
struct SqList{
int r[MAXSIZE+1];//用于存储要排序的数组;r[0]用于哨兵或临时变量;
int length;//顺序表的长度;
};
/*冒泡排序算法*/
void BubbleSort(SqList *L)
{
int i,j;
for(i=1;i<=L->length-1;i++)//n个数据,总共需要n-1趟;0位是哨兵位,从1开始比较
{
for(j=1;j<=L->length-i;j++)//第m趟,需要比较n-m次
{
int tmp;
if(L->r[j]>L->r[j+1])//发生逆序
{
tmp=L->r[j+1];
L->r[j+1]=L->r[j];
L->r[j]=tmp;
}
}
}
}
/**/
void BubbleSortImprove(SqList *L)
{
int i,j;
bool flag=true;//作为是否交换的标记
for(i=1;i<=L->length-1&&flag;i++)//n个数据,总共需要n-1趟;0位是哨兵位,从1开始比较
{
flag=false;
for(j=1;j<=L->length-i;j++)//第m趟,需要比较n-m次
{
int tmp;
if(L->r[j]>L->r[j+1])//发生逆序
{
tmp=L->r[j+1];
L->r[j+1]=L->r[j];
L->r[j]=tmp;
flag=true;//若发生交换置位true;
}
}
}
}
int main()
{
SqList listdata;
listdata.length=10;
default_random_engine e(time(0));
uniform_int_distribution<int>u(0,500);
for(int i=1;i<=10;i++)
{
listdata.r[i]=u(e);
}
for(auto &index:listdata.r)
{
//第一个输出的是哨兵值,所以出现了逆序;
cout<<index<<endl;
}
cout<<endl;
BubbleSortImprove(&listdata);
for(auto &index:listdata.r)
{
//第一个输出的是哨兵值,所以出现了逆序;
cout<<index<<endl;
}
return 0;
}
//----------------------------------------------------------------
void bubble_sort(int arr[], int len)
{
int i, j;
bool flag=true;
for (i = 1; i < len - 1&&flag; i++)
{
flag=false;
for (j = 1; j < len - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
swap(arr[j], arr[j + 1]);
flag=true;//若发生交换置位true;
}
}
}
}
9.2.2 快速排序
#include<iostream>
#include <random>
using namespace std;
#define MAXSIZE 10 //用于要排序的数组的最大个数
struct SqList{
int r[MAXSIZE+1];//用于存储要排序的数组;r[0]用于哨兵或临时变量;
int length;//顺序表的长度;
};
/*快速排序*/
void swap(SqList *L,int i,int j)
{
int tmp=L->r[i];
L->r[i]=L->r[j];
L->r[j]=tmp;
}
int Partition(SqList *L,int low,int high)
{
int pivotkey;
/*优化快速排序*/
//------------------------------------------------------
//三数取中法避免选择的第一个枢轴元素不合理
int m=low+(high-low)/2;//计算数组元素中间元素下标;
if(L->r[low]>L->r[high])
{
swap(L,low,high);
}
if(L->r[m]>L->r[high])
{
swap(L,high,m);
}
if(L->r[m]>L->r[low])
{
swap(L,m,low);
}
//此时L.r[low]已经是整个序列左中右三个的中间值
//------------------------------------------------------
pivotkey=L->r[low];//将线性表第1个位置作为枢轴元素,所有元素与他比较,大的放在右边,小的放在左边;
L->r[0]=pivotkey;//将关键字备份到0位置
while(low<high)
{
while(low<high&&L->r[high]>=pivotkey)
{
high--;
}
L->r[low]=L->r[high];
while(low<high&&L->r[low]<=pivotkey)
{
low++;
}
L->r[high]=L->r[low];
}
L->r[low]=L->r[0];//枢轴元素填入
return low;//此时low==high
}
void QuickSort(SqList *L,int low,int high)
{
int pivotloction;//枢轴元素位置
if(low<high)
{
pivotloction=Partition(L,low,high);//将L.r[low,high]区间一份为二,pivotloction为
//枢轴元素排好序的位置
QuickSort(L,low,pivotloction-1); //对低子表递归排序
QuickSort(L,pivotloction+1,high); //对高子表递归排序
}
}
int main()
{
SqList listdata;
listdata.length=10;
default_random_engine e(time(0));
uniform_int_distribution<int>u(0,500);
for(int i=1;i<=10;i++)
{
listdata.r[i]=u(e);
}
for(auto &index:listdata.r)
{
//第一个输出的是哨兵值,所以出现了逆序;
cout<<index<<endl;
}
cout<<endl;
QuickSort(&listdata,1,listdata.length);
for(auto &index:listdata.r)
{
//第一个输出的是哨兵值,所以出现了逆序;
cout<<index<<endl;
}
return 0;
}
//数组版
//-------------------------------------------------------
#include <iostream>
#include <random>
#include <chrono>
using namespace std;
int pivotPosition(int *arr,int low,int high)
{
arr[0]=arr[low];
while (low<high)
{
while (low<high&&arr[0]<=arr[high])
{
high--;
}
arr[low]=arr[high];
while (low<high&&arr[0]>=arr[low])
{
low++;
}
arr[high]=arr[low];
}
arr[low]=arr[0];//进行一次快排之后中枢的位置;
return low;
}
void fastSort(int *arr,int low,int high)
{
if(low<high)//数据个数大于一个
{
int pivot=pivotPosition(arr,low,high);
//递归左边
fastSort(arr,low,pivot-1);
fastSort(arr,pivot+1,high);
}
}
//非递归实现
void fastSortNo(int *arr, int low, int high)
{
//利用栈存储分块的起始位置
stack<int> st;
if (low < high)
{
int pivot = pivotPosition(arr, low, high);
if (low < pivot - 1) //左半区存在
{
st.push(low);
st.push(pivot - 1);
}
if (pivot + 1 < high) //右半区存在
{
st.push(pivot + 1);
st.push(high);
}
while (!st.empty())
{
int high = st.top();
st.pop();
int low = st.top();
st.pop();
pivot = pivotPosition(arr, low, high);
if (low < pivot - 1) //左半区存在
{
st.push(low);
st.push(pivot - 1);
}
if (pivot + 1 < high) //右半区存在
{
st.push(pivot + 1);
st.push(high);
}
}
}
}
int main()
{
const int number_count=1000000;
default_random_engine e(time(0));
uniform_int_distribution<int> u(0,number_count);
int arr[number_count];
arr[0]=-1;
for(int i=1;i<number_count;i++)
{
arr[i]=u(e);
}
clock_t start,end;
start=clock();
fastSort(arr,1,number_count-1);
end=clock();
system("clear");
for(auto &o:arr)
{
cout<<o<<'\t';
}
cout<<endl;
cout<<"time = "<<(double(end-start)/CLOCKS_PER_SEC)*1000<<"ms"<<endl;
return 0;
}
9.3 选择排序
9.3.1 简单选择排序
1.基本思想:
在待排序数组中选出最小的(或最大)的与第一个位置的数据交换 然后在剩下的待排序数组中找出最小(或最大)的与第二个位置的数据交换,以此类推,直到第n-1个元素。
简单选择排序可以说是冒泡排序的一种改版,它不再两两比较出较小数就进行交换,而是每次遍历比较当前数的后面所有数,最后再把最小的数和当前数进行交换。

#include<iostream>
#include <random>
using namespace std;
#define MAXSIZE 10 //用于要排序的数组的最大个数
struct SqList{
int r[MAXSIZE+1];//用于存储要排序的数组;r[0]用于哨兵或临时变量;
int length;//顺序表的长度;
};
void swap(SqList *L,int i,int j)
{
int tmp=L->r[i];
L->r[i]=L->r[j];
L->r[j]=tmp;
}
/*简单选择排序*/
void SelectSort(SqList *L)
{
int i,j,min;
for(i=1;i<L->length;i++)
{
min=i;//当前下标定义为最小值的下标
for(j=i+1;j<=L->length;j++)
{
if(L->r[min]>L->r[j])
min=j;
}
if(i!=min)//min!=i说明 min是最小值,交换,否则,i就是最小值,不交换;
{
swap(L,i,min);
}
}
}
int main()
{
SqList listdata;
listdata.length=10;
default_random_engine e(time(0));
uniform_int_distribution<int>u(0,500);
for(int i=1;i<=10;i++)
{
listdata.r[i]=u(e);
}
for(auto &index:listdata.r)
{
//第一个输出的是哨兵值,所以出现了逆序;
cout<<index<<endl;
}
cout<<endl;
SelectSort(&listdata);
for(auto &index:listdata.r)
{
//第一个输出的是哨兵值,所以出现了逆序;
cout<<index<<endl;
}
return 0;
}
9.3.2 堆排序
堆两个性质:
- 完全二叉树
- 父节点大于(小于)其左右孩纸–>大顶堆(小顶堆)

/**堆排序
* 本节内容参考视频1:https://www.bilibili.com/video/BV1Eb41147dK?from=search&seid=15876630224781988619
* 本节内容参考视频2:https://www.bilibili.com/video/BV1fp4y1D7cj?from=search&seid=15876630224781988619
*/
#include <iostream>
using namespace std;
/**
* @brief: 对当前节点构造一个大顶堆,通过比较根节点与左右孩子节点的值,将较大的值,放入根节点中;
* @parame:tree:输入数组
* @parame:n:数组大小
* @parame:i:待维护节点的下标
*/
void heapify(int tree[], int n, int i)
{
if(i>=n)//定义一个递归出口函数
{
return ;
}
int maxIndex = i; //父节点
int lson = 2 * i + 1; //左孩子
int rson = 2 * i + 2; //右孩子
if (lson < n && tree[maxIndex] < tree[lson]) //左孩子比父节点大,就行交换
{
maxIndex = lson;
}
if (rson < n && tree[maxIndex] < tree[rson]) //右孩子比父节点大,就行交换
{
maxIndex = rson;
}
if(maxIndex!=i)//说明有一个孩子比父节点值大,所以进行交换;
{
swap(tree[maxIndex],tree[i]);//和其中较大的孩子进行值交换,此时maxindex依然保存的是值较大孩子的索引,
//交换后依然需要进行对该孩子进行堆维护,保证该孩子也是一个堆;
heapify(tree,n,maxIndex); //确保该孩子也是一个堆;
}
}
/** @brief:堆排序入口函数;
*/
void HeapSort(int tree[],int n)
{
//建堆
int i;
int lastNode=n-1;
int parent=(lastNode-1)/2;//parent==>(n/2)-1;
for(i=parent;i>=0;i--)//从最后一个元素的的父节点开始,依次构建堆;
{
heapify(tree,n,i);
}
//排序
for(i=n-1;i>=0;i--)
{
swap(tree[i],tree[0]);//交换最后以元素与堆顶元素;
heapify(tree,i,0);//重新调整堆,从堆顶开始调整;
}
}
int main()
{
int arr[10]={1,5,8,6,9,15,24,45,88,2};
HeapSort(arr,10);
for(int i=0;i<10;i++)
{
cout<<arr[i]<<"\t";
}
cout<<endl;
return 0;
}
9.4 归并排序
归并排序,是创建在归并操作上的一种有效的排序算法。算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。归并排序思路简单,速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。
- 基本思想
归并排序是用分治思想,分治模式在每一层递归上有三个步骤:
* 分解(Divide):将n个元素分成个含n/2个元素的子序列。
* 解决(Conquer):用合并排序法对两个子序列递归的排序。
* 合并(Combine):合并两个已排序的子序列已得到排序结果。
- 实现逻辑
2.1 迭代法
① 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
② 设定两个指针,最初位置分别为两个已经排序序列的起始位置
③ 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
④ 重复步骤③直到某一指针到达序列尾
⑤ 将另一序列剩下的所有元素直接复制到合并序列尾
2.2 递归法
① 将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素
② 将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素
③ 重复步骤②,直到所有元素排序完毕

#include<iostream>
#include<random>
using namespace std;
//!归并排序 其实分成两步==>1.分治(将一个数组划分) 2.合并(合并过程就是排序过程)
//!合并==>即使合并也是排序过程;
void merge(int arr[],int tempArr[],int low,int mid,int high)
{
//标记左半区第一个未排序的位置
int l_pos=low;
//标记左半区第一个未排序的位置
int r_pos=mid+1;
//临时数组元素下标
int pos=low;
//合并
while(l_pos<=mid&&r_pos<=high)
{
if(arr[l_pos]<arr[r_pos])
{
tempArr[pos++]=arr[l_pos++];
}
else
tempArr[pos++]=arr[r_pos++];
}
//合并左半区剩余的元素
while(l_pos<=mid)
{
tempArr[pos++]=arr[l_pos++];
}
//合并右半区剩余的元素
while(r_pos<=high)
{
tempArr[pos++]=arr[r_pos++];
}
//把临时数组中合并后的元素复制回原来的数组
while (low<=high)
{
arr[low]=tempArr[low];
low++;
}
}
//!划分
/**
* @param:arr[]待排序数组;
* @param:tempArr[]辅助数组;
* @param:low;数组第一个元素;
* @param:high数组最后一个元素;
*/
void Split(int arr[],int tempArr[],int low ,int high)
{
//如果只有一个元素,那么就不需要继续划分,只需要归并
if(low<high)
{
//找中间点
int mid=(low+high)/2;
//递归划分左半区域
Split(arr,tempArr,low,mid);//!使用tempArr的low==>mid区域进行排序
//递归划分右半区域
Split(arr,tempArr,mid+1,high);//!使用tempArr的mid+1==>high区域进行排序
//合并
merge(arr,tempArr,low,mid,high);
}
}
//归并排序的入口
void msort(int arr[],int n)
{
//分配一个辅助数组
int *tempArr=new int [n];
if(tempArr)
{
Split(arr,tempArr,0,n-1);
delete []tempArr;
}
}
int main()
{
int arr[] = {9, 5, 2, 7, 12, 4, 3, 1, 11};
int n = 9;
msort(arr, n);
for(auto &index:arr)
{
cout<<index<<endl;
}
int bigData[10000];
default_random_engine e(time(0));
uniform_int_distribution<int> u(0,99999);
int i;
for(i=0;i<10000;i++)
{
bigData[i]=u(e);
}
msort(bigData, 10000);
for(auto &index:bigData)
{
cout<<index<<"\t";
}
return 0;
}
9.5 总结
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
直接插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
希尔排序 | O ( n l o g n ) − O ( n 2 ) O(nlogn)-O(n^2) O(nlogn)−O(n2) | O ( n 1.3 ) O(n^{1.3}) O(n1.3) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
简单选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
堆排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( 1 ) O(1) O(1) | 不稳定 |
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
快速排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n 2 ) O(n^2) O(n2) | O ( l o g n ) − O ( n ) O(logn)-O(n) O(logn)−O(n) | 不稳定 |
归并排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n ) O(n) O(n) | 稳定 |
10.字符串
10.暴力匹配返回index
#include <iostream>
#include <string>
using namespace std;
//暴力匹配:返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为-1;
int index(string &S, string &T, int pos)
{
int i = pos;
int j = 0;
while (i <= S.length() && j <= T.length())
{
if (S[i] == T[j]) //暴力匹配;
{
i++;
j++;
}
else
{
i = i - j + 1; //! 指针后退重新开始匹配;
j = 0;
}
}
if (j > T.length()) //因为匹配成功后会多加一次,如果这个长度大于字符串长度则表示匹配成功;
{
return i - T.length(); //计算index;
}
else
return -1;
}
int main()
{
string str1 = "goodgoogle";
string str2 = "google";
int ret = index(str1, str2, 0);
cout << ret << endl;
return 0;
}