一、数据结构
1、线性表
1.1 线性结构是什么:
- 是一个有序数据元素的集合,常见的线性结构有:线性表、栈、队列、双队列、数组、串
- 数据元素之间存在一对一的线性关系
1.2 线性结构的特点:
- 线性结构有唯一的首元素(第一个元素)
- 线性结构有唯一的尾元素(最后一个元素)
- 除首元素外,所有的元素都有唯一的“前驱”
- 除尾元素外,所有的元素都有唯一的“后继”
- 数据元素存在一对一的关系,除首元素和尾元素外,其他数据元素都是首尾相连的
1.3 线性表的顺序表示:
1.3.1 顺序表的定义(即顺序存储)
-
顺序表的实现–静态分配
#include "stdio.h" #define MAX_SIZE (100) typedef struct { int data[MAX_SIZE]; int length; }SqList; //初始化线性表 void initList(SqList & L) { for(int i = 0; i < MAX_SIZE; i++) { L.data[i] = 0 ; } L.length = 0; }
-
顺序表的实现–动态分配
#include "stdio.h" #define INIT_SIZE (10) typedef struct { int *data; int MaxSize; int length; }SqList; //初始化列表 void initList(SqList &L) { L.data = (int *)malloc(sizeof(int) * INIT_SIZE);//开辟内存 L.MaxSize = INIT_SIZE; L.length = 0; } //增加动态数组长度 void increaseSize(SqList &L,int len) { //实现原理:分三步 //1、保存原来数组指针,后续释放 //2、开辟新的内存空间 //3、拷贝原始数组数据,并释放原始数组内存空间 int *p = L.data; L.data = (int *)malloc(sizeof(int) * (INIT_SIZE + len)); for(int i = 0;i < L.length; i++) { L.data[i] = p[i]; } L.MaxSize = p.MaxSize + len; free(p); }
1.3.2 顺序表上基本操作的实现 (插入和删除)
-
顺序表基本操作——静态链表插入
/*基于静态链表的实现*/ #include "stdio.h" #define MAX_SIZE (10) typedef struct { int data[MAX_SIZE]; int length; }SqList; bool insertElement(SqList &L,int i; int e) { //注意:i代表的是位序,所以位序不能超过length+1 //第3位对应数据下标为2的数据 if(i < 1 || i > L.length+1) return false;//插入超范围 if( L.length >= MAX_SIZE) return false;//内存空间已满 //实现逻辑: //1、插入位置元素后移,空出一个位置进行插入 //2、赋值,链表长度自增1 for(int j = L.length; j >= i; j-- ) { L.data[j] = L.data[j - 1]; } L.data[i - 1] = e; L.lenght++; return true; }
分析时间复杂度:
关注最深层循环语句——L.data[j]=L.data[j-1]的执行次数与问题规模n——L.length的关系;
最好情况:插入表尾,不需要移动元素,i=n+1,循环0次;最好时间复杂度 = O(1)
最坏情况:插入表头,需要将原有的n个元素全都向后移动,i=1,循环n次;最坏时间复杂度 = O(n)
平均情况:假设新元素插入到任何一个位置的概率p(=1/n+1)相同平均循环次数 = np + (n-1)p + (n-2)p + … + 1×p = [ n(n+1)/2 ]×[ 1/(n+1) ] = n/2
平均时间复杂度 = O(n)
-
顺序表基本操作——静态链表删除
/*基于静态链表的实现*/ #include "stdio.h" #define MAX_SIZE (10) typedef struct { int data[MAX_SIZE]; int length; }SqList; bool deleteList(SqList &L,int i,int &e) { //注意:按位删除,即删除第i位元素 = delete data[i-1] if(i <1 || i > L.length) return false; e = L.data[i-1];//保存这个元素 //实现逻辑 //1、除元素后面元素位置上移 //2、链表长度减1 for(int j = i; j < L.length; j++) { L.data[j - 1] = L.data[j] ; } L.length--; }
分析时间复杂度:
关注最深层循环语句——L.data[j-1]=L.data[j]的执行次数与问题规模n——L.length的关系;
最好情况:删除表尾元素,不需要移动元素,i=n,循环0次;最好时间复杂度 = O(1);
最坏情况:删除表头元素,需要将后续的n-1个元素全都向前移动,i=1,循环n-1次;最坏时间复杂度 = O(n);
平均情况:假设删除任何一个元素(1,2,3,…,length)的概率相同 p=1/n平均循环次数 = (n-1)p + (n-2)p + … + 1×p = [ n(n-1)/2 ]×[ 1/(n) ] = n-1/2
平均时间复杂度 = O(n)
-
顺序表基本操作——查找(按位和按值)
1、按位查找,没啥好说的,分两步 //1、 判断i的值是否合法 //2、 return L.data[i-1]; //注意是i-1 // 算法时间复杂度为O(1) 2、按值查找 #include "stdio.h" #define INIT_SIZE (10) typedef struct { ElemType *data; int length; }SqList; bool findList(SqList L,ElemType e) { //实现原理,遍历查找 //可想而知,算法时间复杂度为O(n) for(int i = 0; i < L.length;i++) { if(L.data[i] == e) return i+1;//返回的是位序 else return 0; } }
1.4 线性表的链式表示:
1.4.1 单链表的定义
-
链式存储
-
每个节点存储:数据元素自身信息和指向下一个节点的地址
-
优点:不要求大片连续区域,改变容量方便
-
缺点:不可随机存储,要耗费时间修改指针
-
结构定义
typedef struct LNode { ElemType data;//数据域 //结构体自引用指针,只能用指针,不然大小无法确定,会无限循环 struct LNode *next;//指针域 }LNode,*LinkList; //LNode --- 指代一个节点 //LinkList -- 表示这是一个单链表 LinkList LReal;//创建链表 InitList(LReal);//初始化 /***************************************************************************** 这里LinkList本身就是指针,为何传引用? 1、如果不传地址,则会产生一个形参副本,并不改变LReal本身 2、所以, 想改变LReal的值,必须传入地址。此时LTemp保存的是 LReal的地址,对LTemp的操作即是对LReal的操作 (LinkList <emp = LReal) 3、其实也可以直接创建赋值: LNode * head=(Node *)malloc(sizeof(LNode)); head->data=0; head->next=NULL; LReal = head; Note:typedef struct LNode * LinkList--意思是LinkList的类型是struct LNode * LinkList看成struct Lnode *的别名就行 *****************************************************************************/ //初始化一个空的单链表(不带头节点) bool InitList(LinkList <emp) { LTemp = NULL; //空表,暂时还没有任何结点; return true; } //初始化带头节点的单链表 bool initList(LinkList L) { L = (LNode*)malloc(sizeof(LNode)); if(L == NULL) return false;//内存空间不足 L->next = NULL; return false; } //判断链表是否为空 bool isEmpty(LinkLsit L) { //不带头结点 return (L==NULL) ? true:false; //带头结点 return (L->nexrt == NULL) ? true:false; }
1.4.2 单链表基本操作的实现
-
单链表的插入
a、按位插入----带头结点
//L->head->a1->a2->a3>a4->a5->NULL 头结点看成0节点 (不存在数据) //第i个位置插入数据元素e //实现原理: //1、先判断节点是否合理 //2、第i位插入,即找到i-1那个节点(前一个节点) //3、申请新节点进行插入 bool insertList(LinkList &L,int i,ElemType e) { if(i < 1) return false; LNode*p = L;//指针p进行扫描 int j = 0;//p指向第几个节点,带头节点,头节点可看成第0个节点 //循环遍历,找到前一个节点,即i-1,同时得不能为NULL while(p!= NULL && j< i-1) { p = p->next; j ++; } if(p == NULL) return false;//i值不合法 //申请新节点进行插入 LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; return true; } /******************************* 时间复杂度分析: 最好情况:插入第1个位置 O(1) 最坏情况:插入表尾 O(n) 平均时间复杂度 = O(n) *******************************/
b、按位插入----不带头结点
// L->a1->a2->a3>a4->a5->NULL bool insertList(LinkList &L,int i,ElemType e) { if(i < 1) return false; //如果插入到第一个节点 //插入第一个节点需要改变L的指向,所以要单独写 //不带头结点,链表为空,头指针指向NULL if(i == 1) { LNode * s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = L; L = s; return true; } LNode *p = L; //p指向第几个节点,i位插入即要找到i-1那个节点后进行插入 //注意,这和带头结点不一样,因为第一个节点插入已实现 int j = 1;//侧面表示从第一个节点开始的 while(p!=NULL && j < i-1) { p = p->next; j++; } if(p == NULL) return false; LNode * s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; return true; } /******************************* 时间复杂度分析: 最好情况:插入第1个位置 O(1) 最坏情况:插入表尾 O(n) 平均时间复杂度 = O(n) *******************************/
c、指定节点的后插操作
//给定一个节点p,在其后插入一个元素e(单链表智能向后查找,所以p之前节点无法得知) bool insertNextNode(LNode *p,ElemType e) { if(p == NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); if(s == NULL) return false; s->data = e; s->next = p->next; p->next = s; return true; } //显而易见,平均时间复杂度为O(1)
d、指定节点的后插操作
//如果给定头指针,之前查找p的前驱q,对q进行后插操作即可-------平均时间复杂度O(n) //如果没给头指针?直接把新节点插到p后面,然后交换数据域 -------平均时间复杂度O(1) bool insertPriorNode(LNode *p,ElemType e) { if(p == NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); if(s == NULL) return false; //后插并交换数据域 == 前插 s->next = p->next; p->next = s; s->data = p->data; p->data = e; }
-
单链表的删除
按位删除----带头结点
//删除表L中第i个位置的元素,并用e返回删除元素的值;头结点视为“第0个”结点 //实现原理: //1、找到i-1个节点,与i后面节点进行相连 //2、删除i节点 //3、平均时间复杂度为O(n) bool deleteNode(LinkList &L,int i,ElemType &e) { LNode *p = L; int j = 0;//从“第0个”节点开始扫描 while(p!=NULL && j < i-1) { p = p->next; j ++; } if(p == NULL) return false;//不合法 if(p->next == NULL) return false;//说明i-1后面无节点 LNode *q = p->next;//指向删除节点,便于后续释放 e = q->data;//获取要删除节点数据 p->next = q->next; free(q); return true; }
-
单链表的查找
a、按位查找----带头结点
//查找表中第i个元素的值 //平均时间复杂度 = O(n) LNode* getElem(LinkList L,int i) { if(i < 1) return NULL; LNode *p = L; int j = 0; while(p != NULL && j < i) { p = p->next; j++; } return p; }
b、按值查找----带头结点
//查找值为e的节点 //平均时间复杂度 = O(n) LNode* locateElem(LinkList L,ElemType e) { LNode *p = L->next; while(p!=NULL && p->data != e) { p = p->next; } return p; }
-
单链表的长度
//从头到位遍历循环 //平均时间复杂度 = O(n) int length(LinkList L) { LNode *p = L; int length = 0; while(p->next != NULL) { p = p->next; length++; } return length; }
-
单链表的建立
a、尾插法
//从尾部依次插入每个元素e //简单思路: //1、初始化单链表 //2、设置变量length记录链表当前的长度 //3、每插入一个元素都得循环遍历一次链表,所以时间复杂度位O(n*n) while { //每次取一个数据元素e; ListInsert(L, length+1, e)插到尾部; length++; } //解决方案:增加一个尾指针 LinkList List_TailInsert(LinkList &L) { int X;//假设ElemType为整数型 L = (LNode*)malloc(sizeof(LNode));//创建头结点 if(L == NULL) return NULL; LNode *s, LNode *rear = L;//节点和尾指针 scanf('%d',%&X); while(X!=111) { //插入 s = (LNode*)malloc(sizeof(LNode));//创建结点 s->data = X; rear->next = s;//将s串起来 rear = s;//指向尾节点 scanf('%d',%&X); } rear->next = NULL;//尾指针置空 return L; }
b、头插法
//每次从头部插入元素 LinkList List_HeadInsert(LinkList &L) { int X;//假设ElemType为整数型 L = (LNode*)malloc(sizeof(LNode));//创建头结点 if(L == NULL) return NULL; L->next = NULL;//尾部置空,不可或缺 LNode *s; scanf('%d',%&X); while(X!=111) { //插入 s = (LNode*)malloc(sizeof(LNode));//创建结点 s->data = X; s->next = L->next; L->next = s; scanf('%d',%&X); } return L; }
c、头插法应用----链表逆置
LinkList reverseList(LinkList &L) { LNode *p,*q; p = L->next;//获取链表第一个节点 q = p; L->next = NULL;//链表断开 while(p! = NULL) { q = p; p = p->next; q->next = L->next; L->next = q; } return L; }
1.4.3 双链表(初始化,插入,删除,遍历)
-
双链表的结构
typedef struct DNode { ElemType data; struct DNode *prior,*next;//前驱和后继指针 } DNode,*DLinkList;
-
双链表的初始化
//即创建一个头结点,头结点的前驱始终为NULL,后继节点暂时为NULL bool initDLinkList(DLinkList &L) { L = (DNode *)malloc(sizeof(DNode)); if(L == NULL) return false; L->prior = NULL; L->next = NULL; return true; } //判断链表是否为空 if(L->next == NULL )
-
双链表的插入
//在p节点之后插入s节点 //注意:双链表比较简单,注意插入或删除的节点是否是最后一个节点即可 bool insertNextNode(DNode *p,DNode *s) { if(p == NULL || s == NULL) return false; p->next = s->next; if(p->next != NULL) { //如果是最后一个节点插入,就没有前驱 p->next->prior = s; } s->prior = p; p->next = s; return true; }
-
双链表的删除
//删除p节点的后继节点 //注意:双链表比较简单,注意插入或删除的节点是否是最后一个节点即可 bool deleteNextNode(DNode *p) { if(p == NULL) return false; DNode *q = p->next; if(q == NULL) return false;//没有后继节点可删除 p->next = q->next; if(q->next != NULL) { //连接后继节点的前驱 //如果是最后一个节点,就没有后继节点 q->next->prior = p; } free(q); } //循环释放各个数据节点 void destoryList(DLinkList &L) { while(L->next != NULL) { deleteNextNode(L->next); } //释放头结点(即头指针指向的内存空间清零) free(L); //但是free后头指针仍然指向垃圾内存,所以要将头指针置为NULL //这也是delete和free的一个区别 L = NULL; }
-
双链表的遍历
//遍历的目的是什么?对节点p做相应处理 //后向遍历 while(p != NULL) { p = p->next; } //前向遍历 while(p!=NULL) { p = p->prior; } //前向遍历(跳过头结点) while(p->prior != NULL) { p = p->prior; }
1.4.4 循环链表(即尾指针指向头部)
-
循环单链表
//初始化时,头指针指向自己 L->next = L; //判断结点p是否为循环单链表的表尾结点 bool isTail(LinkList L,LNode *p) { if(p->next == L) return true; else return false; } //判断循环单链表是否为空 bool Empty(LinkList L) { if(L->next == L) return true; else return false; }
-
循环双链表
//初始化时,头指针指向自己 L->prior = L; L->next = L; //判断结点p是否为循环双链表的表尾结点 bool isTail(DLinklist L,DNode *p) { if(p->next == L) return true; else return false; } //判断循环双链表是否为空 bool Empty(DLinklist L) { if(L->next == L) return true; else return false; }
1.4.5 静态链表
//因为链表的空间是离散的,所以静态链表的空间是连续的,类似数组
//没啥可研究的
#define MaxSize 10 //静态链表的最大长度
struct Node{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
1.4.6 顺序表和链表的比较
//主要是内存空间和增删改查时间复杂度的比较
//偷个懒,后续补上......
2、栈、队列、数组
2.1 栈
2.1.1 栈的基本概念
定义:栈是只允许在一端进行插入或者删除操作的线性表(类似羊肉串)
1、栈顶:允许插入和删除的一端
2、栈底:不允许插入和删除的一端
3、特点:后进先出(LIFO--Last In First Out)
2.1.2 栈的顺序存储结构
-
定义
//顺序栈由静态数组和栈顶指针组成,其中栈顶指针存放的是数组下标 #define MAX_SIZE (100) typedef struct { ElemType data[MAX_SIZE]; int top;//栈顶指针 }SqStack; //Sq--sequence 顺序的意思
-
初始化&&判空
//初始化 void initStack(SqStack &S) { S.top = -1; } //判空 bool isEmptyStack(SqStack S) { return ( S.top == -1) ? true : false; }
-
进栈
bool push(SqStack &S,ElemType e) { if(S.top == MAX_SZIE - 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]; S.top--; return true; }
-
读取栈顶元素
bool getTop(SqStack S,ElemType &x) { if(S.top == -1) return false; //栈空,报错 x=S.data[S.top];//x记录栈顶元素 return true; }
-
共享栈
//两个栈共享同一片内存空间 #define MaxSize 10 //定义栈中元素的最大个数 typedef struct { ElemType data[MaxSize]; //静态数组存放栈中元素 int top0; //0号栈栈顶指针 int top1; //1号栈栈顶指针 }SqStack; //初始化栈 void InitStack(ShStack &S) { S.top0=-1; //初始化栈顶指针 S.top1=MaxSize; } //栈满条件 top0+1 = top1;
2.1.3 栈的链式存储结构
//链栈的建立类似于头插法建立链表
typedef struct Linknode
{
ElemType data; //数据域
struct Linknode *next; //指针域
}*LiStack
2.1.4 栈在括号匹配中的应用
//即输入()、{}、[]进行匹配
#define MaxSize 10
typedef struct
{
ElemType data[MAX_SIZE];
int top;
}SqStack;
bool bracketCheck(char []str,int length)
{
SqStack S;
initStack(S);//初始化栈
for(int i = 0; i <length; i++)
{
if(str[i] == '(' || str[i] == '{}' || str[i] == '[]')
{
push(S,str[i]);//入栈
}
else
{
if(isEmptyStack(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 isEmptyStack(S);//检查是否全部匹配完成(即是否为空栈)
}
2.1.5 栈在表达式求值中的应用
//中缀、后缀、前缀表达式
1、中缀表达式:运算符在两个表达式之间 如:a+b
2、前缀表达式:运算符在两个表达式前面 如:+ab
3、后缀表达式:运算符在两个表达式后面 如:ab+
/*********************************************************************************************
* 利用计算机计算算数表达式,只包含加减乘除四则运算,比如:34+13*9+44- 12/3
*
* 思路:通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。
* 1、我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,
* 2、就与运算符栈的栈顶元素进行比较。如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;
* 3、如果比运算符栈顶元素的优先 级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,
* 4、然后进行 计算,再把计算完的结果压入操作数栈,继续比较。
*********************************************************************************************/
//to be continue
2.1.5 栈在递归中的应用
//to be continue
2.2 队列
2.2.1 队列的基本概念
定义:只允许在一端进行插入,另一端进行删除的线性表(类似食堂打饭,ETC)
1、队首、队尾、空队列
2、特点:先入先出(FIFO--First In First Out)
2.2.2 队列的顺序存储结构
-
定义与初始化
#define MAX_SIZE (100) typedef struct { ElemType data[MAX_SIZE]; //一般队头指向第一个元素,队尾指向最后一个元素的下一个位置 int front,rear;//队首和队尾指针 }SqQueue; void InitQueue(SqQueue &Q) { //初始时队头、队尾指针指向0 Q.rear=Q.front=0; } //计算队列长度 int length = (rear + MAX_SIZE - front )%MAX_SIZE; //判空(即队首和队位指向同一个元素,牺牲一个元素的空间) bool isEmptyQueue(SqQueue Q) { return (Q.front == Q.rear) ? true : false; }
-
入队和判满
//队尾入队 //需要牺牲一个空间的元素的,因为队列为空的条件就是队首和队尾指向同一个元素 bool enterQueue(SqQueue &Q,ElemType e) { if((Q.rear + 1)% MAX_SIZE == Q.front) return false;//判断队满 Q.data[Q.rear] = e; Q.rear = (Q.rear + 1)%MAX_SIZE; return true; }
-
出队
//队首出队 //删除一个队首元素 bool deleteQueue(SqQueue &Q,ElemType &e) { if(Q.rear == Q.front) return false;//队空 e = Q.data[Q.front]; Q.front = (Q.front + 1)% MAX_SIZE; return true; }
-
获取队头元素值
//和出队相比,不改变队头指针 bool GetHead(SqQueue Q,ElemType &e) { if(Q.rear == Q.front) return false; //队空 e = Q.data[Q.front]; return true; }
-
不浪费空间的定义方式
//按照以上方式,会造成一个空间的浪费,所以如下方式可进行改进 //方法一初始 #define MaxSize 10 typedef struct { ElemType data[MaxSize]; int front,rear; //初始化时rear=front=0;size=0; //插入成功size++;删除成功size--; //队满条件:size==MaxSize; int size; //队列当前长度 }SqQueue; //方法二 #define MaxSize 10 typedef struct { ElemType data[MaxSize]; int front,rear; //初始化时,rear=front=0;tag=0; //队空条件:front==rear && tag==0 //队满条件:front==rear && tag==1 int tag; }SqQueue;
2.2.3 队列的链式存储结构
-
定义与初始化
//队列的一个节点 typedef struct LinkNode { ElemType data; struct LinkNode *next; } //队列链表 typedef struct { LinkNode *front,*rear; }LinkQueue; //初始化队列 void InitQueue(LinkQueue &Q) { //初始时,front、rear都指向头结点 Q.front = Q.rear = (LinkNode *)malloc(sizeof(LinkNode)); Q.front->next = NULL; } //判空 bool IsEmptyQueue(LinkQueue Q) { if(Q.front==Q.rear) return true; //当队头指针和队尾指针指向同一个元素时,队列为空 else return false; }
-
入队
void enterQueue(LinkQueue &Q,ElemType e) { //实现步骤: //1、创建一个新节点,尾部指向NULL //2、节点插入,尾指针后移 LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode)); s->data = s; s->next = NULL; Q.rear->next = s; Q.rear = s; }
-
出队
void deleteQueue(LinkQueue &Q,ELemType &e) { //实现步骤 //1、删除一般需要释放内存空间,所以先用临时指针指向 //2、删除节点,进行链接,注意判断是不是最后一个节点 LinkLode *p = Q.front->next; x = p->data;//取出数据 Q.front->next = p->next; if(p = Q.rear) Q.rear = Q.front;//判断是否为最后一个数据 free(p); }
2.2.4 双端队列
//to be continue
2.3 数组
1、数组和链表的区别(初级面试必问)
常见回答:链表适合插入、删除,时间复杂度O(1);数组适合查找,查找时间复杂度为O(1)
但是!实际上,这种表述是不准确的。数组是适合查找操作,但是查找的时间复杂度并不为O(1)
即便是排好序的数组,你用二分查找,时间复杂度也是O(logn)
所以,正确的表述应该是,数组支持随机访问,根据下标随机访问的时间复杂度为O(1)
2、什么是数组
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据
3、数组的内存开辟(可以研究下C++的vector容器,很有代表性)
#define MAX_SIZE
栈:ElemType array[MAX_SIZE];//数组类型 数组名 数组大小
堆:
//C语言实现
ElemType *p = (ElemType *)malloc(MAX_SIZE * sizeof(ElemType));
free(p);//释放,释放后p置空(p == NULL),因为free释放的是p指向的内存空间
//C++实现
ElemType *p = new ElemType[MAX_SIZE];
delete []p;//数组删除需要delete[]
//此外
int *p = new int;//表示开辟大小为1个int型空间,并将地址赋值给p
int *p = new int(10);//同上,并把int的值赋为10
3、树
3.1 树的基本概念
树的特点(文件夹、省市图):
①每个节点有零个或多个子节点;
②没有父节点的节点称为根节点;
③每一个非根节点有且只有一个父节点;
④除了根节点外,每个子节点可以分为多个不相交的子树;
基础术语:度,叶子节点,根节点,父节点,子节点,深度,高度。
结点的层次(深度):从上往下数(默认从1开始)
结点的高度:从下往上数
树的高度(深度):总共有多少层
重点:
结点的度:有几个孩子(分支)-- 结点拥有的子树个数称为结点的度
树的度:各结点度的最大值
3.2 二叉树的概念
二叉树是n(n>=0)个结点的有限集合:
①或者为空二叉树,即n=0
②或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。
特点:①每个节点至多两棵子树 ②有序树(左右子树不能颠倒)
1 、满二叉树
①只有最后一层有叶子结点
②不存在度为1的结点
③按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点i的父结点为[i/2](如果有的话)
2、完全二叉树(相当于去掉满二叉树的大编号)
①只有最后两层有叶子结点
②最多只有一个度为1的结点
③按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点i的父结点为[i/2](如果有的话)
④i<=[n/2]为分支结点,i>n/2为叶子结点
3、二叉排序树(BST)
①左子树上所有结点的关键字都比根节点小
②右子树上所有结点的关键字都比根节点大
③左子树和右子树又是一棵二叉树
4、平衡二叉树(AVL树)
①树上任一结点的左子树和右子树深度之差不超过1
3.3 二叉树的存储结构
3.3.1 二叉树的顺序存储
//只适合存储完全二叉树,不然会造成大量空间浪费
#define MAX_SIZE (100)
struct TreeNode
{
ElemType value;//结点的数据元素
bool isEmpty;//结点是否为空
}
TreeNode t[MAX_SIZE] ;
3.3.2 二叉树的顺序存储
//链式存储----n个结点的二叉链表共有n+1个空链域
typedef struct BiTNode
{
ElemType data;
struct BiTNode *lchild,*rchild;
/*struct BiTNode *parent; //父结点指针,三叉链表,方便找父节点*/
}BiTNode,*BiTree;
//定义一棵空树
BiTree root = NULL;
//插入根节点
root = (BiTree)malloc(sizeof(BiTNode));
root->data = {1};
root->lchild = NULL;
root->rchild = NULL;
//插入新节点
BiTNode *p = (BiTNode*)malloc(sizeof(BiTNode));
p->data = {2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p;
3.3.3 二叉树先、中、后序遍历
//先序遍历----根左右(NLR)
void preOrder(BitTree T)
{
if(T != NULL)
{
visit(T);//访问根结点
preOrder(T->lchild);//递归访问左子树
preOrder(T->rchild);//递归访问右子树
}
}
//中序遍历----左根右(LNR)
void inOrder(BitTree T)
{
if(T != NULL)
{
inOrder(T->lchild);//递归访问左子树
visit(T);//访问根结点
inOrder(T->rchild);//递归访问右子树
}
}
//后序遍历----左右根(LRN)
void postOrder(BitTree T)
{
if(T != NULL)
{
postOrder(T->lchild);//递归访问左子树
postOrder(T->rchild);//递归访问右子树
visit(T);//访问根结点
}
}
//空间复杂度O(h+1)--O(h)
3.3.4 二叉树层序遍历
算法思想:
1、初始化一个辅助队列
2、根结点入队
3、如队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
4、重复3直至队列为空
//二叉树的结点(链式存储)
typedef struct BiTNode
{
char data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BitTree;
//链式队列结点
typedef struct LinkNode
{
BiTNode *data;//ElemType
struct LinkNode *next;
}LinkNode;
typedef struct
{
LinkNode *front,*rear;
}LinkQueue;
//层次遍历
void levelOrder(BiTree T)
{
LinkQueue Q;
InitQueue(Q);//初始化辅助队列
enterQueue(Q,T);//根结点入队
while(!isEmptyQueue(Q))
{
BiTree p;//保存出队的元素(ElemType &e)
deleteQueue(Q,p);//队头结点出队
visit(p);
//左右子结点入队,如果存在的话
if(p->lchild != NULL) enterQueue(Q,p->lchild);
if(p->rchild != NULL) enterQueue(Q,p->rchild);
}
}
3.3.5 遍历序列构建二叉树
//如果只给出一个二叉树的 前/中/后/层序遍历中的一种, 不能唯一确定一棵二叉树
3.3.6 线索二叉树
//二叉树的结点
typedef struct BiTNode
{
ElemType data;
struct BiTNode *lchild,*rchild;
/*struct BiTNode *parent; //父结点指针,三叉链表,方便找父节点*/
}BiTNode,*BiTree;
//线索二叉树的结点----保存前驱+后继
typedef struct BiTNode
{
ElemType data;
struct BiTNode *lchild,*rchild;
//tag == 0 表示指针指向孩子
//tag == 1 表示指针指向"线索"
int ltag,int rtag; //左右线索标记
}BiTNode,*BiTree;
//to be continue
3.4 树的存储结构
3.5 树和森林的遍历
//先根遍历:若树非空,先访问根结点,再依次对每棵子树进行先根遍历;(与对应二叉树的先序遍历序列相同)
void PreOrder(TreeNode *R)
{
if(R!=NULL)
{
visit(R); //访问根节点
while(R还有下一个子树T) PreOrder(T); //先根遍历下一个子树
}
}
//后根遍历:若树非空,先依次对每棵子树进行后根遍历,最后再返回根节点;(与对应二叉树的中序遍历序列相同)
void PostOrder(TreeNode *R)
{
if(R!=NULL)
{
while(R还有下一个子树T) PostOrder(T); //后根遍历下一个子树
visit(R); //访问根节点
}
}
//层次遍历(队列实现)
若树非空,则根结点入队;
若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队;
重复以上操作直至队尾为空;
3.6 二叉排序树(BST)
二叉排序树:又称二叉查找树(BST, Binary Search Tree)
①左子树上所有结点的关键字都比根节点小
②右子树上所有结点的关键字都比根节点大
③左子树和右子树又是一棵二叉树
-
二叉排序树查找
//二叉排序树结点 typedef sruct BSTNode { int key; struct BSTNode *lchild,*rchild; }BSTNode,*BSTree; //在二叉树查找值为key的结点 //最坏空间复杂度O(1) BSTNode *BST_Search(BSTree T,int key) { while(T != NULL && key != T->key) { if(key < T->key) T = T->lchild; else T= T->rchild; } return T; } //在二叉树查找值为key的结点(递归实现) //最坏空间复杂度O(n) BSTNode *BST_Search(BSTree T,int key) { if(T == NULL) return NULL; if(key == T->key) return T;//查找成功 else if(key < T->key) return BST_Search(T->rchild,key);//左子树中查找 else return BST_Search(T->rchild,key);//右子树中查找 }
-
二叉排序树插入
//在二叉排序树插入关键字为key的新结点(递归实现) int BST_Insert(BSTree &T,int k) { //原树为空,新插入的结点为根结点 if(T == NULL) { T = (BSTree)malloc(sizeof(BSTNode)); T->key = k; T->lchild = T->rchild = NULL; return 1; } else if(k = T->key) { rerurn 0;//树中存在相同关键字的结点,插入失败 } else if(k < T->key) { return BST_Insert(T->lchild,k);//插到T的左子树 } else if(k > T->key) { return BST_Insert(T->rchild,k);//插到T的右子树 } }
-
二叉排序树构造、删除
//按照str[]中的关键字序列建立二叉排序树 //str = {10,20,30,44,55,59,23,15,18} void Create_BST(BSTree &T,int str[],int n) { T = NULL;//初始时T为空树 int i = 0; while(i < n) { BST_Insert(T,str[i]); i++; } } //删除 //1、如果结点Z是叶子结点,直接删除 //2、如果结点Z是一棵左子树或者右子树,则让Z的子树称为父结点的子树,替代Z的位置 //3、如果结点Z有一棵左子树&&右子树(可以研究下) //查找效率ASL(Average Search Length) //对比关键字次数不会超过二叉树高度,反映了时间复杂度 //最好情况:n个结点的二叉树最小高度为log2n+1,平均查找长度为O(logn+1) //最坏情况:每个结点只有一个分支,n个结点的二叉树高度=结点数n,平均查找长度为O(n) //所以需要 /*---平衡二叉树!!!----*/
3.7 平衡二叉树(AVL)
平衡二叉树的产生是为了解决二叉排序树在插入时发生线性排列的现象。由于二叉排序树本身为有序,当插入一个有序程度十分高的序列时,生成的二叉排序树会持续在某个方向的字数上插入数据,导致最终的二叉排序树会退化为链表,从而使得二叉树的查询和插入效率恶化。
平衡二叉树的出现能够解决上述问题,但是在构造平衡二叉树时,却需要采用不同的调整方式,使得二叉树在插入数据后保持平衡。主要的四种调整方式有LL(左旋)、RR(右旋)、LR(先左旋再右旋)、RL(先右旋再左旋)。这里先给大家介绍下简单的单旋转操作,左旋和右旋。LR和RL本质上只是LL和RR的组合。
平衡二叉树(Balanced Binary Tree):简称平衡树(AVL树)--树上任一结点的左子树和右子树高度差不超过1
结点的平衡因子=左子树高-右子树高
在插入一个结点后应该沿搜索路径将路径上的结点平衡因子进行修改,当平衡因子大于1时,就需要进行平衡化处理。从发生不平衡的结点起,沿刚才回溯的路径取直接下两层的结点,如果这三个结点在一条直线上,则采用单旋转进行平衡化,如果这三个结点位于一条折线上,则采用双旋转进行平衡化
时间复杂度:
普通二叉树----树高为h,查找一个关键字最多需要比对h次,即查找的时间复杂度不超过O(h)
平衡二叉树----含有n个结点的平衡二叉树最大深度为O(log2 N),所以平均查找长度为O(log2 N)(n表示结点数)
3.8 红黑树
平衡二叉树(AVL)为了追求高度平衡,需要通过平衡处理使得左右子树的高度差必须小于等于1。高度平衡带来的好处是能够提供更高的搜索效率,其最坏的查找时间复杂度都是O(logN)。但是由于需要维持这份高度平衡,所付出的代价就是当对树种结点进行插入和删除时,需要经过多次旋转实现复衡。这导致AVL的插入和删除效率并不高。
红黑树具有五个特性:
1、每个结点要么是红的要么是黑
2、根结点是黑的
3、每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的
4、如果一个结点是红的,那么它的两个儿子都是黑的
5、对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点
3.9 哈弗曼树
//1、结点的权
//2、结点的带权路径长度:从根节点到该结点的路径长度(经过的边数)与该结点上权值的乘积
//3、树的带权路径长度:树中所有 叶子结点 带权路径长度之和
哈弗曼树:在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树,也称最优二叉树
4、图
4.1 图的基本操作
//删除&&插入
4.2 图的广度遍历优先(BFS)
//即层序遍历--Breadth First Search
//1、找到一个与顶点相邻的所有顶点
//2、标记哪些顶点被访问过
//3、需要一个辅助队列
bool bVisited[MAX_VERTEX_NUM];//访问标记数组
FirstNeighbor(G,x)
-----求图G中顶点的第一个邻接点,如果有返回顶点号,
-----若X没有邻接点或图中不存在x,则返回-1
NextNeighbor(G,x,y)
-----假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点的下一个邻接点的顶点号,
-----若y是x的最后一个邻接点,则返回-1
//从顶点v出发,广度遍历图G
void BFS(Graph G,int v)
{
visit(v);//访问初始顶点v
bVisited[v] = true;//进行标记
enterQueue(Q,v);//入队
while(isEmptyQueue(Q))
{
deleteQueue(Q,v);
for(w = FirstNeighbor(G,v); w >= 0;w = NextNeighbor(G,v,w))
{
//检测v所有邻接点
if(!bVisited[v])
{
visit(w);//访问顶点w
bVisited[w] = true;//进行标记
enterQueue(Q,w);//顶点w入队列
}
}
}
}
//但是如果是非连通图,则无法遍历完所有顶点
//所以增加一个对标记数组的访问
void BFSTraverse(Graph G)
{
for(int i = 0; i < G.Vexnum; i++)
{
bVisited[i] = false;//初始化
}
initQueue(Q);//初始化辅助队列
//从0号顶点开始遍历
for(int i = 0; i < G.Vexnum; ++i)
{
if( !bVisited[i] )
BFS(G,i);//vi没访问过,从vi开始BFS
}
}
//空间复杂度取决于辅助队列 ----- 最坏情况,辅助队列大小为(|V|)
//1、领接矩阵----时间复杂度 O(|V|^2)
//2、领接表----时间复杂度 O(|V|+|E|)
4.3 图的深度遍历优先(DFS)
//深度优先遍历--Depth First Search
void DFS(Graph G,int v)
{
visit(v);
bVisited[v] = true;
for(w = FirstNeighbor(G,v); w >= 0;w = NextNeighbor(G,v,w))
{
if( !bVisited[w] )
DFS(G,w);//递归调用
}
}
//同理,针对非连通图作一下改进
void DFSTraverse(Graph G)
{
for(int v = 0; v < G.Vexnum;v++)
{
bVisited[i] = false;//初始化
}
initQueue(Q);//初始化辅助队列
//从0号顶点开始遍历
for(int v = 0; v < G.Vexnum; ++v)
{
if( !bVisited[v] )
DFS(G,v);//vi没访问过,从vi开始BFS
}
}
4.4 最小生成树
//连通图的生成树是包含图中全部顶点的一个极小连通子图
MST--Minimum Spannine Tree
1、Prim算法(普里姆)--适合于边稠密图
从某个顶点开始构建生成树木,每次将代价最小的新顶点纳入生成树木,直到所有顶点都纳入为止
2、Kruskal算法(克鲁斯卡尔)--适合于边稀疏图
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的都不选),直到所有顶点连通
4.5 最短路径问题
单源最短路径
BFS算法(无权图)
Dijkstra算法(带权图、无权图)
每对顶点间的最短路径
Floyd算法(带权图、无权图)
1、BFS算法
//适合不带权的图
时间复杂度:O(n^2)
//广度优先算法
bool bVisited[MAX_VERTEX_NUM];//访问标记数组
//从顶点v出发,广度遍历图G
void BFS(Graph G,int u)
{
for(int i = 0;i < G.vexnum;i++)
{
d[i] = ∞;//初始化路径长度
path[i] = -1;//最短路径从哪个顶点过来
}
d[u] = 0;
visit(u);//访问初始顶点u
bVisited[u] = true;//进行标记
enterQueue(Q,u);//入队
while(isEmptyQueue(Q))
{
deleteQueue(Q,u);
for(w = FirstNeighbor(G,u); w >= 0;w = NextNeighbor(G,u,w))
{
//检测v所有邻接点
if(!bVisited[u])
{
d[w] = d[u] + 1;
path[w] = u;
bVisited[w] = true;//进行标记
enterQueue(Q,w);//顶点w入队列
}
}
}
}
2、Dijkstra算法–迪杰斯特拉
//会手动测算即可---不适合带负权值的图
时间复杂度:O(n^2)
3、Floyd算法
//使用动态规划思想,将问题的求解分为多个阶段---不适合带负权值的图
//......准备工作,根据图的信息初始化矩阵A和path
空间复杂度:O(n^2)
时间复杂度:O(n^3)
for(int k = 0; k < n; k++)
{
//考虑以Vk作为中转点,遍历矩阵,i,j分别为行号、列号
for(int i = 0; i < n; i++)
{
for(int j = 0; j < n; j++)
{
//是否以Vk作为中转点路径更短
if(A[i][j] > A[i][k] + A[k][j])
{
A[i][j] = A[i][k] + A[k][j];//更新最短路径
path[i][j] = k;//更新中转点
}
}
}
}
4、拓扑排序
bool TopologicalSort(Graph G)
{
initStack(s);//初始化栈,存放度为0的顶点
for(int i = 0; i < G.vexnum; i++)
{
if(indegree[i] == 0)
push(S,i);//将所有入度为0的顶点进栈
int count = 0;//计数,记录当前已经输出的顶点数
while(isEmpty(S))
{
pop(S,i);
print(count++) = i;//输出顶点i
for(p = G.vertices[i].firstarc; p; p= p->nextarc)
{
//将所有i指向的顶点的入度减1,并且将入度减为0的顶点压入栈S
v = p->adjvex;
if(! (--indegree[v]) )
push(S,v);//入度为0,则入栈
}
}
//如;排序失败---有向图中有回路
return (count < G.vexnum) ? false : true;
}
}
时间复杂度:O(|V|+|E|)
采用领接矩阵:O(V^2)
5、查找
***ASL(Average Search Length)😗**所有查找过程需要进行关键字比较次数的平均值
5.1 顺序查找
//又叫线性查找,通常用于线性表
typedef struct
{
ElemType *elem;
int TableLen;
}SSTable;
//顺序查找
int search_Seq(SSTable ST,ElemType key)
{
ST.elem[0] = key;//0号位置存放哨兵
for(int i = ST.TableLen;ST.elem[i] != key;--i)//从后往前找
{
return i;
}
}
5.2 折半查找
//适合有序的顺序表
typedef struct
{
ElemType *elem;
int TableLen;
}SSTable;
//折半查找
int search_Seq(SSTable L,ElemType key)
{
int low = 0,high = L.TableLen - 1,mid;
while(low <= high)
{
mid = (low + high) / 2;
if(L.elem[mid] == key) return mid;
else if(L.elem[mid] > key) high = mid - 1;
else if(L.elem[mid] < key) low = mid + 1;
}
return -1;//查找失败
}
//折半查找判定树一定是平衡二叉树,只有最下面一层是不满的
//因此,数据元素为n时的树高 h = log2(n+1)
//折半查找的时间复杂度为O(log2N)
5.3 分块查找
//分块查找,又称索引顺序查找,算法过程如下:
1、在索引表中确定待查记录所属的分块(可顺序、可折半)
2、在块内顺序查找
//索引表中保存每个分块的最大关键字和分块的存储区间
typedef struct
{
ElemType maxValue;
int low,high;
}Index;
//顺序表的存储实际元素
ElemType List[100];
5.4 B树(平衡多路二叉树)
B树和AVL树(平衡二叉树) 的差别就是 B树 属于多叉树,又名平衡多路查找树,即一个结点的查找路径不止左、右两个,而是有多个。数据库索引技术里大量使用者B树和B+树的数据结构。一个结点存储多个值(索引)。
B树的阶数:M阶表示 一个B树的结最多有多少个查找路径(即这个结点有多少个子节点)。M=M路,M=2是二叉树,M=3则是三叉树。
//Notes:若每个结点的关键字太少,导致树变高,要查更多层结点,效率低
//怎么理解:每个 非叶子结点 的值(索引) 个数 = 子节点个数 -1 。最小为 Math.ceil(M/2)-1 最大为 M-1 个。
//例如:5阶查找树最多有四个索引,将【n,m】划分为5个区间
一棵M阶B树有以下特点:
1. 每个结点的值(索引) 都是按递增次序排列存放的,并遵循左小右大原则。
2. 根结点 的 子节点 个数为 [2,M]。
3. 除 根结点 以外 的 非叶子结点 的子节点个数 为[ Math.ceil(M/2),M]。 Math.ceil() 为向上取整。
4. 每个 非叶子结点 的值(索引) 个数 = 子节点个数 -1 。最小为 Math.ceil(M/2)-1 最大为 M-1 个。
5. B树的所有叶子结点都位于同一层。(即所有子树的高度相同)
//B+树类似于分块查找
//在B+树中,非叶结点不含有该关键字对应记录的存储地址
//可以使一个磁盘可以包含更多的关键字,使得B+树的阶更大,树高更矮
//读磁盘次数更少,效率更快
B+树和B树最大的不同是:
1、B+树内部有两种结点,一种是索引结点,一种是叶子结点。
2、B+树的索引结点并不会保存记录,只用于索引,所有的数据都保存在B+树的叶子结点中。而B树则是所有结点都会保存数据。
3、B+树的叶子结点都会被连成一条链表。叶子本身按索引值的大小从小到大进行排序。即这条链表是 从小到大的。多了条链表方便范围查找数据。
4、B树的所有索引值是不会重复的,而B+树 非叶子结点的索引值 最终一定会全部出现在 叶子结点中。
B树好处:
B树的每一个结点都包含key(索引值) 和 value(对应数据),因此方位离根结点近的元素会更快速。(相对于B+树)
B树的不足:
不利于范围查找(区间查找),如果要找 0~100的索引值,那么B树需要多次从根结点开始逐个查找。
而B+树由于叶子结点都有链表,且链表是以从小到大的顺序排好序的,因此可以直接通过遍历链表实现范围查找。
5.5 散列查找(空间换时间)
散列表(Hash Table):数据元素的关键字和其存储地址直接相关
散列函数H(Key) = Key%n Addr = H(Key)
查找长度:需要对比关键字的次数
装填因子a = 表中记录数/散列表长度(会直接影响散列表的查找效率)
设计更好的散列函数(让不同关键字的冲突尽可能地少)
1、 除留余数法——H(Key) = Key % p (散列表长度为m,m为一个不大于m但最接近或等于m的质数p)
2、 直接定址法——H(Key) = a*key + b(a和b为常数)
3、 数字分析法——选取数码分布较为均匀的若干位作为散列地址
4、 平方取中法——取关键字的平方值的中间几位作为散列地址
另有——开放定址法:线性探测法、平方探测法、伪随机序列法
6、内部排序(八大排序)
6.1 直接插入排序
//算法思想:
//每次将一个待排序的记录按其关键字的大小插入到
//前面已经排好序的子序列中,直达全部记录插入完成
//直接插入排序
//空间复杂度:O(1)
//时间复杂度:O(n^2)
void InsertSort(A[],int n)
{
int i,j,temp;
for(i = 1;i<n;i++)
{
if(A[i] < A[i-1])
{
temp = A[i];//暂存i,方便插入
for(j = i-1;j >=0 && A[j] > temp;j--)
{
A[j+1] = A[j];//所有大于temp的元素向后挪位
}
A[j] = temp;//复制到插入位置
}
}
}
//优化——折半插入排序
void InsertSort(int A[],int n)
{
int i,j,low,mid,high;
for(i = 2;i<n;i++)
{
A[0] = A[i];//哨兵模式
low=1,high = i-1;//设置折半查找范围
while(low<=high)
{
mid = (low+high)/2;
if(A[mid] > A[0]) high = mid -1;//查找左半子表
else low = mid +1;//查找右半子表
}
for(j = i-1;j >= high+1;j--)
{
A[j+1] = A[j];//所有大于temp的元素向后挪位
}
A[high+1] = A[0];//复制到插入位置
}
}
6.2 希尔排序(Shell Sort)
//算法思想:先追求部分有序,再逼近全局有序
//先将待排序表分割成若干形如L[i,i+d,i+2d,......,i+kd]的特殊子表,对各个子表分别
//进行直接插入排序。缩小增量d,重复上述过程,直到d=1为止
//希尔排序(仅适用于顺序表,不适用链表)
//空间复杂度:O(1)
//时间复杂度:未知,但是优于直接插入排序
void shellSort(int A[],int n)
{
int d,i,j;
//A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
for(d = n/2, d >= 1; d = d/2)
{
for(i = d + 1; i <= n; ++i)
{
//需要将A[i]插入到有序增量子表中
if(A[i] < A[i-d])
{
A[0] = A[i];//暂存在A[0]
//子表直接插入排序
for(j = i-d; j > 0 && A[0] < A[j]; j-=d)
{
A[j+d] = A[j];//记录后移,查找插入的位置
}
A[j+d] = A[0];//插入
}
}
}
}
6.3 冒泡排序
//属于交换排序
//算法思想:
//从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1] > A[i]),则交换它们,直达序列比较完。
//称这样过程“一趟”冒泡排序
//交换
void swap(int &a,int &b)
{
int temp = a;
a = b;
b = temp;
}
//冒泡排序
//空间复杂度:O(1)
//时间复杂度:O(n^2)
//该方法是往前冒泡,每次把最小的冒出来
void bubbleSort(int A[], int n)
{
//n-1即N个元素只需要排N-1次
for(int i = 0; i < n-1; i++)
{
bool flag = false;//表示本趟冒泡是否发生交换的标志
//一趟冒泡过程
for(int j = n-1; j > i; j++)
{
//若为逆序
if(A[j-1] > A[j])
{
swap(A[j-1],A[j]);
flag = true;
}
if(flag == true) return;//如果遍历后未发生交换,表示表已经有序
}
}
}
6.4 快速排序
//属于交换排序
//算法思想:
//1、主要是通过一趟排序将待排记录分隔成独立的两部分
//2、其中的一部分比关键字小,后面一部分比关键字大
//3、然后再对这前后的两部分分别采用这种方式进行排序
//4、通过递归的运算最终达到整个序列有序
//快速排序
//空间复杂度:O(递归层数)
//时间复杂度:O(Nlog2N)
void quickSort(int A[],int low,int high)
{
if(low < high)
{
int pivotops = partition(A,low,high);//划分
quickSort(A,low,pivotops-1);//划分左子表
quickSort(A,lpivotops+1,high);//划分右子表
}
}
//用第一个元素将待排序序列划分成左右两个部分
int partition(int A[],int low,int high)
{
int pivot = A[low];//用第一个元素作为枢轴
while(low < high)
{
while(low<high && A[high] > pivot) --high;//找出小于基准的元素就停止
A[low] = A[high];
while(low<high && A[low] < pivot) ++low;//找出大于基准的元素就停止
A[high] = A[low];
}
A[low] = pivot;
return low;
}
6.5 简单选择排序
//属于选择排序
//每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列
//简单选择排序
//空间复杂度:O(1)
//时间复杂度:O(n^2)
void selectSort(int A[], int n)
{
//n个元素进行n-1趟
for(int i = 0;i < n-1;i++)
{
int min = i;//记录最小元素位置
//在A[i,..n-1]中选择最小的元素
for(int j = i+1;j < n;j++)
{
if(A[j] < A[min]) min = j;
}
if(min != i) swap(A[i],A[min]);//如果i不是最小元素,则交换
}
}
6.6 堆排序、堆的插入排序
//若n个关键字序列L(1,2,3..n)满足下面某一条性质,则称为堆(Heap)
//1、如满足:L(i) >= L(2i)且L(i) >= L(2i+1)(1<= i <= n/2)——大根堆:完全二叉树中,根>=左、右
//2、如满足:L(i) <= L(2i)且L(i) <= L(2i+1)(1<= i <= n/2)——小根堆:完全二叉树中,根<=左、右
//属于选择排序
//算法思想:把所有非终端节点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整
//1、检查当前节点是否满足根>=左右,如不满足,将当前节点与更大的一个孩子进行互换
//2、如元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断下坠)
//建立大根堆
void buildMaxHeap(int A[],int len)
{
for(int i = len/2; i>0; i--)
headAdjust(A,i,len);
}
//将以k为根的子树调整为大根堆
void headAdjust(int A[],int k;int len)
{
A[0] = A[k];//A[0]暂存子树的根结点
//沿k较大的子结点向下筛选
for(int i = 2*k;i <= len; i*=2)
{
if(i < len && A[i] < A[i+1]) i++;//取key较大的子结点的下标
if(A[0] >= A[i]) break;//筛选结束
esle
{
A[k] = A[i];//将A[i]调整到双亲结点上
k = i;//修改k值,以便于向下筛选
}
}
A[k] = A[i];//被筛选的结点的值放入最终位置
}
//堆排序
//空间复杂度:O(1)
//建堆时间复杂度:O(n)
//时间复杂度:O(nlog2n)
//a、每一趟将堆顶元素加入有序子序列(与待排序序列中的最后一个元素进行交换)
//b、并将待排序元素序列再次调整为大根堆(小元素不断下坠)
void HeapSort(int A[],int len)
{
buildMaxHeap(A,len);
//n-1趟的交换和建堆过程
for(int i = len; i >1; i--)
{
swap(A[i],A[1]);//堆顶元素和堆底元素交换
headAdjust(A,1,i-1);//将剩余待排序元素整理成堆
}
}
6.7 归并排序
//Merge Sort
//将多个已经有序的序列合并成一个
//m路归并,每选出一个元素需要对比关键字m-1次
int *B = (int *)malloc(n*sizeof(int));//辅助数组B
//A[low...mid]和A[mid...high]各自有序,将两部分归并
void merge(int A[],int low,int mid,int high)
{
int i,j,k;
//将A中所有元素复制到B中
for(k = low; k <= high; k++)
B[k] = A[k];
//归并
for(i = low; j = mid+1,k = i; i <= mid && j <= high;k++)
{
if(B[i] <= B[j])
A[k] = B[i++];//将较小值复制到A中
else
A[k] = B[j++];
}
//检查剩余元素,并归并到尾部
while(i <= mid) A[k++] = B[i++];
while(j <= high) A[k++] = B[j++];
}
//归并排序
//空间复杂度:O(n)——来源于辅助数组B
//每趟归并时间复杂度:O(n)
//时间复杂度:O(nlog2n)
void MergeSort(int A[],int low,int high)
{
if(low < high)
{
int mid = (low + high)/2;//从中间划分
MergeSort(A,low,mid);//对左半部分归并排序
MergeSort(A,mid+1,high);//对右半部分归并排序
merge(A,low,mid,high);//归并
}
}
6.8 基数排序
//Radix Sort——并不是基于“比较”的排序算法
//1、初始化:设置r个空队列,Qr-1,Qr-2,....Q0;
//2、按照各个关键字权重递增的次序(个、十、百),对d个关键字位分别做“分配”和“收集”
//3、分配:顺序扫描各个元素,若当前处理的关键字位=x,则将元素插入Qx队尾
//4、收集:把Qr-1,Qr-2,....Q0各个队列中的结点依次出队并链接
typedef struct LinkNode
{
ElemType data;
struct LinkNode *next;
}LinkNode,*LinkList;
typedef struct
{
LinkNode *front,*rear;
}LinkQueue;
需要r个辅助队列,空间复杂度 = O(r)
一趟分配O(r),总共d趟分配、收集,总的时间复杂度 = O(d(n+r))