算法度量
时间复杂度
事先预估算法时间开销 T(n) 与问题规模 n 的关系.用O( )来体现算法时间复杂度的记法,称之为大O记法
推到大O阶:
- 用常数1取代运行时间中所有加法常数
- 在修改后的运行次数函数中,只保留最高阶项
- 如果最高阶项存在且不是1,则去除与这个项相乘的常数
常用的时间复杂度所耗费的时间从小到大依次是:
空间复杂度
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n) = O(f(n)) , 其中,n为问题的规模,f(n)为语句关于n所占的存储空间的函数。
若算法执行所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为O(1).
线性表
定义
操作
顺序表
定义
实现
静态分配
//顺序表的插入操作 时间复杂度O(n)
bool ListInsert(SqList &L, int i, int e){
if(i<1||i>L.length+1) //判断i的范围是否有效
return false;
if(L.length >= MaxSize) //存储空间满了,不能插入
return false;
for(int j = L.length;j>=i;j--){ //将第i个元素之后的元素后移
L.data[j] = L.data[j-1];
}
L.data[i-1] = e;
L.length++; //长度加1
return true;
}
动态分配
#define InitSize 10 //默认长度
typedef struct{
int *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList;
int main(){
SeqList L;
InitList(L); //初始化
//...插入元素
IncreaseSize(L,5);
return 0;
}
void InitList(SeqList &L){
L.data = (int *)malloc(InitSize*sizeof(int));
L.length = 0;
L.MaxSize = InitSize;
}
//增加动态数组的长度
void IncreaseSize(SeqList &L,int len){
int *p = L.data;
L.data = (int *)malloc((L.MaxSize+len)*sizeof(int));
for(int i = 0;i < L.length;i++){ //将数据复制到新区域
L.data[i] = p[i];
}
L.MaxSize = L.MaxSize + len; //更新最大长度
free(p); //释放原来的内存空间
}
特点
- 随机访问,即可以在O(1)时间内找到第i个元素
- 存储密度高,每个节点只存储数据元素
- 扩展容量不方便
- 插入删除不方便,需要移动大量的元素
链表
优点:不要求大片连续空间,改变容量方便
缺点:不可随机存取,要耗费一定空间存放指针
单链表
//定义一个单链表
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
//初始化
bool InitList(LinkList &L){
L = NULL; //空表,暂时还没有节点
return true;
}
//判断链表是否为空
bool Empty(LinkList L){
return (L==NULL);
}
插入
//后插操作 在p节点之后插入元素e
bool InsertNextNode(Node *p,ElemType e){
if(p == NULL)
return false;
LNode *node = (LNode *)malloc(sizeof(LNode));
if(node == NULL)
return false; //内存分配失败
node->data = e;
node->next = p->next;
p->next = node;
return ture;
}
//在第i个位置前插入元素e(带头结点)
bool ListInsert(LinkList &L,int i,ElemType e){
if(i < 1)
return false;
LNode *p; //指向当前扫到的节点
int index = 0;
p = L;
while(p != NULL && index<i-1){ //循环找到第i-1个节点
p = p->next;
index++;
}
return InsertNextNode(p,e); //调用后插函数
}
不带头节点的链表插入到i=1位置要用一段单独的逻辑代码实现,要修改链表指针,使用带头节点的指针更方便。
//前插操作: 在p节点之前插入元素e
bool InSertPriorNode(LNode * p,ElemType e){
if(p==NULL)
return false;
LNode* node = (LNode*)malloc(sizeof(LNode));
if(node == NULL)
return false;
node->next = p->next;
node->data = p->data; //将p中的元素复制给新的节点node
p->next = node;
p->data = e; //p中元素覆盖为e
}
删除
//删除指定节点p(替罪羊) 此方法有bug,对于要删除最后一个节点会出错
bool DeleteNode(LNode *p){
if(p==NULL)
return false;
LNode* q = p->next;
p->data = q->data;
p->next = q->next;
free(q);
return true;
}
// 删除指定值的节点
bool DeleteElem(LinkList &L, ElemType data){
LNode* p , *q;
p = L;
q = p->next; //指向p的下一个节点
while(q->data!=data&&q!=NULL){
q = q->next;
p = p->next;
}
if(q == NULL)
return false; //没有查到这个值
p->next = q->next;
free(q);
return true;
}
双链表
//定义一个双链表
typedef struct DNode {
ElemType e;
struct Dnode *prior,*next;
}DNode,*DLinklist;
//初始化双链表
bool InitDLinklist(DLinklist &L){
L = (DNode*)malloc(sizeof(DNode)); //分配一个头节点
if(L==NULL)
return false;
L->prior = NULL;
L->next = NULL;
return true;
}
插入
//在p节点之后插入s节点
bool InsertNextDNode(DNode *p,DNode *s){
if(p==NULL||s==NULL)
return false;
if(p->next!=NULL) //如果p节点有后继节点的话
p->next->prior = s;
s->next = p->next;
s->prior = p;
p->next = s;
return true;
}
删除
//删除p节点的后继节点
bool DeleteNextDNode(DNode *p){
if(p==NULL)
return false;
DNode* q = p->next; //找到p的后继节点q
if(q==NULL)
return false;
if(q->next!=NULL) //如果q不是最后一个节点
q->next->prior = p;
p->next = q->next;
free(q);
return true;
}
//删除链表
bool DestroyList(DLinklist &L){
while(L->next!=NULL)
DeleteNextDNode(L);
free(L); //释放头节点
L=NULL;
}
静态链表
用数组的方式实现链表,内存地址连续的一片空间:
//定义一个静态链表
#define MAXSIZE 10
typedef struct{
ElemType e;
int next; //下一个元素的数组下标
} SLinklist[MAXSIZE];
SLinklist a; // a 就是一个长度为10的静态链表
初始化时把 a0 设置为 -1 代表是最后一个节点,将所有的next设置为一个特殊的值标识为空闲节点
特点
- 离散的小空间分配方便,改变容量方便
- 不可随机存取,存储密度低
栈和队列
栈
栈是一个只允许在一端进行插入或删除操作的线性表
特点:后进先出(LIFO)
顺序栈实现
//定义
typedef struct {
ElemType data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶下标
} SqStack;
//初始化
void InitStack(SqStack &S){
S.top = -1;
}
//判断是否为空
bool IsEmpty(SqStack &S){
if(S.top == -1)
return true;
else
return false;
}
//新增元素入栈
bool Push(SqStack SqStack,ElemType e){
if(S.top == MaxSize - 1) //栈满,入栈失败
return false;
S.top += 1; //指针先加一
S.data[S.top] = e; //新元素入栈
return true;
}
//出栈
bool Pop(SqStack &S,ElemType &e){
if(S.top == -1)
return false;
e = S.data[S.top--];
return true;
}
//读栈顶元素
bool GetTop(SqStack &S,ElemType &e){
if(S.top == -1)
return false;
e = S.data[S.top];
return true;
}
顺序栈缺点是大小不可改变,可以使用链式栈能在大小上更加灵活,与链表的实现类似,只是只能在头节点加入和删除元素
栈的应用
括号匹配
关键点:最后出现的左括号会最先被匹配(LIFO)
思路:遇到左括号就入栈,遇到右括号就出栈一个左括号进行匹配
匹配失败情况:
- 出栈的左括号和右括号不匹配
- 扫描到右括号时栈底为空
- 扫描结束后,栈非空
算法实现:
bool bracketCheck(char str[],int length){
SqStack S;
InitStack(S); //初始化栈
for(int i = 0;i<str.length;i++){
if(str[i]=='('||str[i]=='['||str[i]=='{')
Push(&S,str[i]);
}else{
if(StackEmpty(&S))
return false;
char topElem;
Pop(&S,&topElem);
if(str[i]==')' && topElem!='(')
return false;
if(str[i]==']' && topElem!='[')
return false;
if(str[i]=='}' && topElem!='{')
return false;
}
}
return StackEmpty(&S); //检测完全部括号后栈空说明匹配成功
}
表达式求值
中缀表达式转后缀表达式
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符
从左到右处理各个元素,直到末尾,可能遇到三种情况:
- 遇到操作数。直接加入后缀表达式
- 遇到界限符。遇到
(
直接入栈;遇到)
则依次弹出栈内运算符并加入后缀表达式,直到弹出(
为止 - 遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到
(
或栈空则停止。之后再把当前运算符入栈
按照上述方法处理完所有字符后,将栈中剩余元素运算符依次弹出,并加入后缀表达式
后缀表达式的计算
- 从左往右扫描下一个元素,直到处理完所有元素
- 若扫描到操作数则压入栈,并回到1,否则执行3
- 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1
若表达式合法,则最后栈只会剩下一个元素,就是最终结果
中缀表达式的计算
结合上面两个算法,就可以实现中缀表达式计算
首先,初始化两个栈,操作数栈和运算符栈
若扫描到操作数,压入操作数栈
若扫描到操作符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
队列
队列是只允许在一端进行插入,在另一端删除的线性表
特点:先进先出(FIFO)
队列的链式实现
//定义
struct LinkNode{
ElemType data;
LinkNode *next;
};
struct LinkQueue{
LinkNode *front,*rear;
};
//初始化队列(带头节点)
void InitQueue(LinkQueue &Q){
//初始时,front,rear都指向头节点
Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
Q.front -> next = NULL;
}
//判断队列是否为空
bool IsEmpty(LinkQueue &Q){
return Q.front == Q.rear;
}
//新元素入队
void EnQueue(LinkQueue &Q,ElemType e){
LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode));
s.data = e;
s.next = NULL;
Q.rear->next = s;
Q.rear = s;
}
//队头元素出队
bool DeQueue(LinkQueue &Q,ElemType &e){
if(Q.front == Q.rear)
return false;
LinkNode *p = Q.front->next;
x = p.data;
Q.front->next = p->next;
if(Q.rear==p)
Q.rear = Q.front;
free(p);
return true;
}
二叉树
定义
- 满二叉树:在满二叉树中除叶子结点外的内部结点,都有两个非空的子结点
- 完全二叉树:从根节点开始,每一层按照从左到右的顺序依次水平填充树,叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部
性质
-
非空二叉树上的叶子结点数等于度为2的结点数加1,即n0 = n2 + 1
-
非空二叉树第k层上至多有2k-1个结点(k>=1)
-
具有n(n>0)个结点的完全二叉树的高度为 ⌊log2n⌋+1 或 ⌈log2n+1⌉
存储
顺序存储
用一组连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nItf8Cis-1610014812658)(顺序存储.png)]
对于一棵完全二叉树,这样存储的好处是数组下标直接对应某个结点。可以根据性质找到其双亲或孩子结点。
对于一棵非完全二叉树,添加一个不存在的空结点,在数组中可以用0表示
弊端:顺序存储最坏情况下会非常浪费存储空间,比较适合完全二叉树
链式存储
用链表来存放一棵二叉树,二叉树中的每个结点用链表的一个链接点来存储
含有n个结点的二叉链表中,有n+1个空链域( 2n - (n - 1) )
遍历
按某条路径访问树中的每个结点,树的每个结点均被访问一次
先序遍历
若二叉树非空:
- 访问根结点
- 先序遍历左子树
- 先序遍历右子树
//递归算法
void PreOrder(BiTree T){
if(T != NULL){
Visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
中序遍历
若二叉树非空:
- 中序遍历左子树
- 访问根结点
- 中序遍历右子树
//递归算法
void InOrder(BiTree T){
if(T != NULL){
InOrder(T->lchild);
Visit(T);
InOrder(T->rchild);
}
}
后序遍历
若二叉树非空:
- 后序遍历左子树
- 后序遍历右子树
- 访问根结点
//递归算法
void PostOrder(BiTree T){
if(T != NULL){
PostOrder(T->lchild);
PostOrder(T->rchild);
Visit(T);
}
}
层序遍历
算法思想:
- 初始将根入队并访问根结点,然后出队
- 如有左子树,则将左子树的根入队
- 如有右子树,则将右子树的根入队
- 然后出队,访问该结点
- 反复该过程直至队列为空
void LevelOrder(BiTree T){
InitQueue(Q);
BiTree p;
EnQueue(Q,T);
while(!isEmpty(Q)){
DeQueue(Q,p);
Visit(p);
if(T->lchild!=NULL)
EnQueue(Q,p->lchild);
if(T->rchild!=NULL)
EnQueue(Q,p->rchild);
}
}
由遍历构造二叉树
先(后)序遍历序列和中序遍历序列可以唯一确定一棵二叉树
先序遍历序列和后序遍历序列不可以唯一确定一棵二叉树
线索二叉树
由上述的遍历可以知道一棵树可以被唯一指定成一种序列,为了加快寻找前序结点和后序结点的速度,引入了线索二叉树。
我们已知,在链式存储结构中,含有n个结点的二叉链表中,有n+1个空链域,为了使空间不能浪费,对二叉树进行线索化:
- 若无左子树,则将指针指向其前驱结点
- 若无右子树,则将指针指向其后继结点
在常规的结构中,由于一个结点只有两个指针域,我们无法得知指针指向的是左/右孩子结点还是前驱/后继节点。所以还需要增加两个标志位说明
对于ltag
和rtag
,有如下规定:
- 为0,则指向左/右孩子结点
- 为1,则指向前驱/后继结点
typedef struct ThreadNode {
ElemType data;
struct ThreadNode * lchild, *rchild;
int ltag,rtag;
}TreadNode,*ThreadTree;
通常比较常用的是中序线索二叉树
线索化
void InThread(ThreadTree &p , ThreadNode* &pre) {
if(p!=NULL){
InThread(p->lchild,pre);
if(p->lchild == NULL){
p->lchild = pre;
ltag = 1;
}
if(pre!=NULL && pre->rchild==NULL){
pre->rchild = p;
rtag = 1;
}
p = pre;
InTread(p->rchild,pre);
}
}
void CreateInThread(ThreadTree T){
ThreadNode* pre = NULL;
if(T!=NULL){
InThread(T,pre);
pre->rchild = NULL;
rtag = 1;
}
}
应用
二叉查找树(BST)
也称二叉排序树,当树非空时有以下特点:
- 若左子树非空,则左子树上所有结点关键字均小于根结点关键字
- 若右子树非空,则右子树上所有结点关键字均大于根结点关键字
- 左、右子树分别也是一棵二叉排序树
二叉排序树的中序遍历序列是一个递增有序序列
查找
- 二叉树非空时,查找根结点,若相等则查找成功
- 若不等且小于根结点的值,查找左子树;若大于根结点的值,查找右子树
- 当查找到叶子结点仍未找到,则查找失败
BSTNode* BST_Search(BiTree T,ElemType key,BSTNode* &p){
p = NULL; //根节点无双亲结点
if(T!=NULL && key != T->data){
p = T;
if(key < T->data)
T = T->lchild;
else
T = T->rchild;
}
return T
}
插入
- 若二叉排序树为空,则直接插入结点
- 若非空且值小于根节点时,插入左子树;大于根节点时,插入右子树;等于根节点时,不进行插入
bool BST_Insert(BiTree &T,KeyType key){
if(T == NULL){
T = (BiTree)malloc(sizeof(BSTNode));
T->lchild = T->rchild = NULL;
T->data = key;
}else if(T->data == key){
return false;
}else if(T->data < key){
return BST_Insert(T->lchild.key);
}else{
return BST_Insert(T->rchild.key);
}
}
删除
- 若被删除结点是叶子结点,则直接删除
- 若被删除结点z只有一棵子树,则让z的子树称为z父结点的子树,代替z结点
- 若被删除结点z有两棵子树,则让z的中序序列的直接后继代替z,然后删去直接后继结点
平衡二叉树(AVL)
任意结点的左子树高度与右子树高度之差的绝对值不超过1
判断
void Judge_AVL(BiTree T,bool &balance,int &height){
bool bl = br = false;
int hl = hr = 0;
if(T == NULL){
height = 0;
balance = true;
}else{
Judge_AVL(T->lchild,bl,hl);
Judge_AVL(T->rchild,br,hr);
if(hl>hr)
h = hl + 1;
else
h = hr + 1;
if(abs(hl-hr)<=1 && bl==true && br==true)
balence = true;
else
balence = false;
}
}
插入
按照二叉查找树的插入方法进行插入,完成后再调整最小的不平衡二叉树,最终形成一个平衡二叉树
哈夫曼树
树的带权路径长度WPL为树中树中所有叶子结点的带权路径长度之和,其中带权路径长度最小的二叉树称为哈夫曼树
构造
- 将n个结点作为n棵仅含有一个根结点的二叉树,构成森林F
- 生成一个新结点,并从F中找出根结点权值最小的两棵树作为它的左右子树,且新结点的权值为两棵子树根结点的权值之和
- 从F中删除这两棵树,并将新生成的树加入到F中
- 重复步骤2、3,直到F中只有一棵树为止
性质
- 每个初始结点都会成为叶子结点,双支结点都为新生成的结点
- 权值越大离根结点越近,权值越小离根节点越远
- 没有节点的度为1
- n个叶子结点的哈夫曼树的结点总数为2n-1,其中度为2的结点总数为n-1
树
树的基本概念
-
树是n(n>=0)个结点的有限集合,n=0时,称为空树
-
n个结点的树中有n-1条边
-
树中一个结点的子结点的个数称为该结点的度;树中最大的度数称为数的度
-
度大于0的节点称为分支结点,度为0的节点称为叶子结点
-
树中两个节点之间的路径是由这两个结点之间所经过的结点序列构成的,路径一定是自上而下的
-
路径上所经历的边数是路径长度
-
m(m>=0)棵互不相交的树的集合称为森林
树的性质
- 树中的结点数等于所有结点的度数和加1
- 度为m的树中第i层上至多有mi-1个结点(i>=1)
- 高度为h的m叉树至多有(mh-1)/(m-1)个结点
- 具有n个结点的m叉树的最小高度为 logm(n(m-1)+1) ,取上界
树的存储结构
双亲表示法
采用一组连续的存储空间来存储每个结点,同时在每个结点中增设一个伪指针,指示双亲结点在数组中的位置。根结点的下标为0,其伪指针域为-1
typedef struct{
ElemType data;
int parent;
}PNode;
typedef struct{
PNode nodes[MAX_SIZE];
int n;
}PTree;
孩子表示法
将每个孩子结点都用单链表连接起来形成一个线性结构,n个结点具有n个孩子链表
typedef struct{
int child; //孩子节点的下标
CNode* next; //指向下一个孩子节点
}CNode;
typedef struct{
ElemType data;
CNode* child;
}PNode;
typedef struct{
PNode nodes[MAX_SIZE];
int n;
}CTree;
孩子兄弟表示法
以二叉链表作为树的存储结构,又称二叉树表示法(左孩子右兄弟)
typedef struct {
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,CSTree;
树的遍历
先根遍历
若树非空,则先访问根结点,再按从左到右的顺序遍历根结点的每棵子树
树的先根遍历序列与这棵树对应的二叉树的先序遍历序列相同
后根遍历
若树非空,则按从左到右的顺序遍历根结点的每棵子树,再访问根结点
树的后根遍历序列与这棵树对应的二叉树的中序遍历序列相同
排序
概念
稳定性
若待排序表中有两个元素Ri和Rj,其对应的值Ki=Kj,且在排序前,Ri在Rj前面,若使用排序算法后,Ri仍在Rj前面,则称这个排序算法是稳定的,否则排序算法不稳定
算法的稳定性是算法的性质,并不能衡量一个算法的优劣
复杂度
时空复杂度决定内部排序算法的性能
插入排序
每次将一个待排序的元素插入到一个前面已经排好的序列当中
void InsertSort(int A[],int len){
for(int i = 1;i < len;i++){
int key = A[i];
for(int j = i-1;j>=0&&A[j]>key;j--){
A[j+1] = A[j];
}
A[j+1] = key;
}
}
空间复杂度:O(1)
最好时间复杂度:O(n)
平均时间复杂度:O(n2)
最坏时间复杂度:O(n2)
稳定性:稳定
希尔排序
希尔排序是插入排序的一种更高效的改进版本
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
希尔排序的基本思想是:先将整个待排序的序列按下标的一定增量分割成为若干子序列分别进行直接插入排序,然后依次缩减增量再进行排序,直到增量为1时,进行最后一次直接插入排序,排序结束
初次取序列的一半为增量,以后每次减半,直到增量为1
void ShellSort(int arr[],int len){
for(int d = len/2;d>0;d/=2){
for(int i = d;i<len;i++){
int key = arr[i];
int j;
for(j = i-d;j>=0&&arr[j]>key;j-=d){
arr[j+d] = arr[j];
}
arr[j+d] = key;
}
}
}
空间复杂度:O(1)
平均时间复杂度:O(n1.5)
最坏时间复杂度:O(n2)
稳定性:不稳定
冒泡排序
比较相邻的元素。如果第一个比第二个大,就交换他们两个
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
void BubbleSort(int arr[],int len){
for(int i = 0;i<len-1;i++){
for(int j = len-1;j>i;j--){
if(arr[j]<arr[j-1]){
int temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
}
}
}
}
空间复杂度:O(1)
最好时间复杂度:O(n)
平均时间复杂度:O(n2)
最坏时间复杂度:O(n2)
稳定性:稳定
选择排序
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
void SelectSort(int arr[],int len){
for (int i = 0; i < len - 1; ++i) {
int min = i;
for (int j = i+1; j < len; ++j) {
if (arr[j]<arr[min]){
min = j;
}
}
if (min!=i){
int temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
}
}
空间复杂度:O(1)
时间复杂度:O(n2)
稳定性:不稳定
归并排序
void Merge(int arr[],int low,int mid,int high){
int * temp = (int *)malloc(sizeof(arr));
int i,j,k;
for (int i = low; i <= high; ++i) {
temp[i] = arr[i];
}
for (i = low,j = mid + 1,k = low; i <= mid&&j<=high; ++k) {
if (temp[i]<=temp[j])
arr[k] = temp[i++];
else
arr[k] = temp[j++];
}
while (i<=mid)
arr[k++] = temp[i++];
while (j<=high)
arr[k++] = temp[j++];
free(temp);
}
void MergeSort(int arr[],int low,int high){
if (low<high){
int mid = (low+high)/2;
MergeSort(arr,low,mid);
MergeSort(arr,mid+1,high);
Merge(arr,low,mid,high);
}
}
空间复杂度:O(n)
时间复杂度:O(nlog2n)
稳定性:稳定
快速排序
基本思想:在待排序表中选取一个元素pivot作为基准,通过一趟排序将待排序表划分为具有如下特点的两部分:
算法步骤:
初始化标记low为第一个元素的位置,high为最后一个元素的位置:
- high向前移动找到第一个比pivot小的元素
- low向后移动找到第一个比pivot大的元素
- 交换当前两个元素的位置
- 重复执行上述步骤直到low等于high为止
int Partition(int arr[],int low,int high){
int pivot = arr[low];
while (low<high){
while (low < high && arr[high] >= pivot)
high--;
arr[low] = arr[high];
while (low < high && arr[low]<=pivot)
low++;
arr[high] = arr[low];
}
arr[low] = pivot;
return low;
}
void QuickSort(int arr[],int low,int high){
if (low<high){
int pivotPos = Partition(arr,low,high);
QuickSort(arr,low,pivotPos);
QuickSort(arr,pivotPos+1,high);
}
}
最好、平均空间复杂度:O(log2n)
最坏空间复杂度O(n)
最好、平均时间复杂度:O(nlog2n)
最坏时间复杂度O(n2) (在有序或逆序时)
稳定性:不稳定
堆排序
堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点
大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列
小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列
算法步骤:
-
构建大根堆(小根堆)
-
把堆首和堆尾互换
-
把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置
-
重复步骤 2,直到堆的尺寸为 1
void AdjustDown(int arr[],int k,int last){
int temp = arr[k];
for (int i = k*2+1; i <= last; i*=2) {
if (i+1<=last && arr[i]<arr[i+1])
i++;
if (temp>=arr[i])
break;
else{
arr[k] = arr[i];
k = i;
}
}
arr[k] = temp;
}
void BuildMaxHeap(int arr[],int len){
for (int i = len/2-1; i >= 0; --i) {
AdjustDown(arr,i,len-1);
}
}
void HeapSort(int arr[],int len){
BuildMaxHeap(arr,len);
for (int i = len-1; i >=0 ; --i) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
AdjustDown(arr,0,i-1);
}
}
空间复杂度:O(1)
时间复杂度:O(nlog2n)
稳定性:不稳定
基数排序
基本思想:将整数按位数切割成不同的数字,然后每个位数分别比较
具体做法:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位一直到最高位排序完成以后, 数组就变成一个有序序列
/*
* 对数组按照某个"位"进行排序(桶排序)
*
* 参数说明:
* a -- 数组
* n -- 数组长度
* exp -- 指数。对数组a按照该指数进行排序。
*
* 例如:
* 当exp=1表示按照"个位"对数组a进行排序
* 当exp=10表示按照"十位"对数组a进行排序
* 当exp=100表示按照"百位"对数组a进行排序
* ...
*/
void radix(int a[], int n, int exp)
{
int output[n]; // 存储"被排序数据"的临时数组
int i, buckets[10] = {0};
// 将数据出现的次数存储在buckets[]中
for (i = 0; i < n; i++)
buckets[ (a[i]/exp)%10 ]++;
// 更改buckets[i]。目的是让更改后的buckets[i]的值,是该数据在output[]中的位置。
for (i = 1; i < 10; i++)
buckets[i] += buckets[i - 1];
// 将数据存储到临时数组output[]中
for (i = n - 1; i >= 0; i--)
--output[buckets[ (a[i]/exp)%10 ]] = a[i];
// 将排序好的数据赋值给a[]
for (i = 0; i < n; i++)
a[i] = output[i];
}
void radix_sort(int a[], int n)
{
int exp; // 指数。当对数组按各位进行排序时,exp=1;按十位进行排序时,exp=10;...
int max = get_max(a, n); // 数组a中的最大值
// 从个位开始,对数组a按"指数"进行排序
for (exp = 1; max/exp > 0; exp *= 10)
radix(a, n, exp);
}
稳定性:稳定
图
图G由顶点集V和边集E组成,记为G = (V,E),顶点个数为图的阶数,线性表,树都可以为空,但图不能为空
生成树
连通图包含全部顶点的一个极小连通子图(含有最少的边)
n个顶点图的生成树有n-1条边
存储
邻接矩阵法
性质
- 空间复杂度为O(n2),适用于稠密图(边多)
- 无向图的邻接矩阵为对称矩阵
- 无向图第i行(列)非0元素的个数为结点i的度
- 有向图第i行(列)非0元素的个数为结点i的出(入)度
- An [i] [j]表示从顶点Vi到顶点Vj长度为n的路径的数目
邻接表法
如果边的数目比较少的话,适用邻接矩阵法会造成空间的浪费
所以,可以为每个顶点建立一个单链表存放与它相邻的边
顶点表
采用顺序存储,每个数组元素存放顶点的数据和边表的头指针
边表
采用链式存储,单链表中存放与一个顶点相邻的所有边,一个链表结点表示一条从该顶点到链表结点顶点的边
下面是一个有向图的例子:
性质
- 若G为无向图,存储空间为O(|V| + 2|E|)
- 若G为有向图,存储空间为O(|V| + |E|)
- 若G为无向图,则结点的度为该结点边表的长度
- 若G为有向图,则结点的出度为该结点边表的长度,计算入度要遍历整个邻接表
- 邻接表不唯一,边表结点的顺序根据算法和输入的不同可能会不同
例题
//判断二叉树是否相等的函数
bool isEqual(BTree T1,BTree T2){
if(T1 == NULL && T2 == NULL)
return true;//都为空,相等。
if(!T1||!T2) //由于上面的判断不成立,则T1,T2至少有一个不为空
return false;//一个空,一个不空,不相等
if(T1->data == T2->data) //如果根节点相等
return isEqual(T1->lc,T2->lc) && isEqual(T1->rc,T2->rc);//判断左右子树是否都相等
else
return false;
}
//判断是否为完全二叉树
bool isCompleteTree(TreeNode* root){
if(root == null) return true;
queue<TreeNode *> queue;
queue.push(root);
while (!queue.empty()){
{
TreeNode* cur=queue.front();
queue.pop();
if(cur==null)
{
break;
}else{
queue.push(cur->left);
queue.push(cur->right);
}
}
//判断队列中剩余的元素是否为空
while (!queue.empty()){
{
TreeNode* cur=queue.front();
queue.pop();
if(cur!=null) return false;
}
return true;
}
//判断是否为满二叉树
bool isFullTree(TreeNode* root){
if(root == NULL) //空树是满二叉树
return true;
else if(root->lchild==NULL&&root->rchild==NULL)//一个结点是满二叉树
return true;
else if(root->lchild==NULL||root->rchild==NULL) //只有一个非空子结点,不是满二叉树
return false;
else
return isFullTree(root->lchild) && isFullTree(root->rchild);
}
// 二叉树叶子节点个数(递归方法)
int leaf_num(BiTree T){
if (T == NULL)
return 0;
if (T->lchild == NULL && T->rchild == NULL)
return 1;
return (leaf_num(T->lchild) + leaf_num(T->rchild));
}
//合并两个有序单链表
LinkedList MergeList(LinkedList A,LinkedList B){
Node *p,*q,*r;
q=A;
p=B->next;
while(p){
while((q->next)&&(q->next->data<p->data)){
q=q->next;
}
r=p;
p=p->next;
r->next=q->next;
q->next=r;
}
return A;
}
//逆序一个单链表
Node * ReverseList(Node *head){
Node *next;
Node *prev = NULL;
while(head != NULL){
next = head->next;
head->next = prev;
prev = head;
head = next;
}
return prev;
}