目录
1.快速排序算法
//初始时,L:最左边元素,R:最右边元素
//该函数最后返回枢轴元素的最终位置
int huafen(int A[],int L,int R){
int mid=A[L]; //设数组最左边的元素为枢轴元素
while(L<R){
while(A[R] > mid) R--;
A[L]=A[R];
while(A[L] < mid) L++;
A[R]=A[L];
}
A[L]=mid;
return L;
}
Qsort(int A[],int L,int R){
if(L==R) return; //递归终止
int M=huafen(A,L,R); //枢轴元素下标
Qsort(A,L,M-1);
Qsort(A,M+1,R);
}
以上代码的缺陷:
① 默认了枢轴元素在数组中间位置:
考虑枢轴元素在边缘位置,例如M指向0,那么M-1,指向-1。这样的话,调用Qsort(A,L,M-1);传入的参数为L=0,M-1=-1,就会出错。
② 在划分函数中,没有考虑其他元素与枢轴元素相等的情况。例如,对于下面的数组,划分函数会出现死循环:
//初始时,L:最左边元素,R:最右边元素
//该函数最后返回枢轴元素的最终位置
int huafen(int A[],int L,int R){
int mid=A[L]; //设数组最左边的元素为枢轴元素
while(L<R){//针对问题②,L<R可以保证L和R移动过程中不会移出数组边界
while(A[R] >= mid &&L<R) R--;
A[L]=A[R];
while(A[L] <= mid &&L<R) L++;
A[R]=A[L];
}
A[L]=mid;
return L;
}void Qsort(int A[],int L,int R){
if(L>=R) return; //针对问题①
int M=huafen(A,L,R); //枢轴元素下标
Qsort(A,L,M-1);
Qsort(A,M+1,R);
}
下面例题就直接用QSort()了
例题1:
int func(int A[],int N,int B[],int M){
int C[N+M];
for(int i=0;i<N;i++);
C[i]=A[i];
for(int i=0;i<M;i++);
C[i+N]=B[i];
Qsort(C,0,N+M-1); //使用快速排序算法
return(C[(N+M-1)/2]);//计算中位数
}
//时间复杂度:
O((N+M)log2(N+M))
快速排序空间复杂度O(nlog2n)
//空间复杂度:
O(M+N)
快速排序空间复杂度为O(log2n),但是由于C的加入,C数组空间复杂度O(M+N)>O(log2n)
例题2:
思路:若主元素存在,那么排序后的数组的中间元素一定是主元素,因为主元素肯定是占数组大半的元素:
int func(int A[],int N){
QSort(A,0,N-1);
int mid=A[N/2];
int count=0;
//从中间元素向左统计相同元素个数
for(int i=N/2-1;i>=0,i--){
if(A[i]==mid) count++;
}
//从中间元素向右统计相同元素个数
for(int i=N/2;i<n,i++){
if(A[i]==mid) count++;
}
if(count>2/N) return mid;
else return -1;
}
时间复杂度O(Nlog2N)
空间复杂度O(log2N)
思路:将无序数组排列为有序数组,找到数组中第一个正整数:
如果找不到,直接返回1。
① 如果这个正整数≠1,则直接返回1
② 如果这个正整数=1,检查A[i+1]-A[i]>1,如果满足,则返回A[i]+1;
③ 若所有元素都不满足,则返回最后一个元素+1
例如,第一个数组应该返回3,第二个数组应该返回5。
int func(int A[],int N){
Qsort(A,0,N-1);
int m=-1;
for(int i=0;i<N;i++){
if(i>0) m=i;
break;
}
if(m==-1 || A[m]!=1)
return 1;
for(int m=m+1;m<N;m++){
if(A[m]-A[m-1]>1)
return A[m-1]+1;
}
return A[N-1]+1; //A数组最后一个元素+1
}
//注:上面第二个for循环也可以写为:
for(int m=m;m<N-1;m++){
if(A[m+1]-A[m]>1)
return A[m]+1;
}
return A[N-1]+1; //A数组最后一个元素+1
//考试还是尽量写A[m]-A[m-1]不容易出错
时间复杂度O(nlog2n),空间复杂度O(log2n)
2.利用划分函数思想解题
“划分”函数返回值=M(数组下标),说明此次选取的枢轴元素是数组中第 M+1 小的元素
(1)找第k小的元素:
int func(int A[],int n,int k){
int L=0,R=n-1,m=0;
while(1){
m=huafen(A,L,R);
//中间元素的下标就是要找的第k个元素,即下标为k-1的元素
if(m==k-1) break;
//如果m>k-1,则第k个元素比中间元素小
else if(m>k-1) R=m-1;
else if(m<k-1) L=m+1;
}
return A[k-1];
}
空间复杂度O(1)
时间复杂度:
第一次调用划分函数,L=0,R=n-1,需要将R与L之间的数据都处理一遍,时间复杂度为n,下一次为2/n,依次类推:
补充:
如果要求数组下标从1开始:
int func(int A[],int n,int k){
int L=1,R=n,m=0;
while(1){
m=huafen(A,L,R);
if(m==k) break;
else if(m>k) R=m-1;
else if(m<k) L=m+1;
}
return A[k];
}
例题:
思路:将数组划分为更小的一半以及更大的一半,也就是说,将数组排序,然后平分数组。若数组元素为8个,那么前四个为一组,后四个为一组;若数组元素为7个,则前3个为一组,后三个为一组。
找第n/2小的元素,也就是数组下标n/2-1元素
int func(int A[],int n){
int k=n/2;
int L=0,R=n-1,m=0;
while(1){
m=huafen(A,L,R);
if(m==k-1) break;
else if(m>k-1) R=m-1;
else if(m<k-1) L=m+1;
}
return A[k-1];
}
3.归并排序
如果题目给了一个乱序数组,需要排成有序数组,那么使用快速排序。如果题目给了多个有序数组,需要合并为一个有序数组,那么使用归并排序。
int Merge(int A[],int N,int B[],int M,int C[]){
int i=0,j=0,k=0;
while(i<N && j<M){
if(A[i]<=B[j]) C[k++]=A[i++];
else C[k++]=B[j++];
}
while(i<N) C[k++]=A[i++];
while(j<M) C[k++]=B[j++];
return 1;
}
例题:
int func(int A[],int N,int B[],int M){
int C[M+N];
Merge(A,N,B,M,C);
return C[M+N/2];
}
4.单链表
按位序查找:
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
//求单链表的长度
int listLen(LinkList L){
int length=0;
LNode *p=L->next;
while(p!=NULL){
length++;
p=p->next;
}
printf("链表的长度=%d\n",length);
return length;
}
//返回单链表中间结点
LNode *findMidNode(LinkList L){
int length=0;
LNode *p=L->next;
while(p!=NULL){
length++;
p=p->next;
}
int count=0;
p=L->next; //从头遍历
while(p!=NULL){
count++;
if(count==length/2)//注意这里不能在p=p->next后面
break;
p=p->next;
}
return p;
}
例题1:
typedef struck LNode{
int data;
struct LNode *next;
}LNode,*LinkList;
int Search_k(LinkList L,int k){
int length=0;
LNode *p=L->next;
while(p!=NULL){
length++;
p=p->next;
}
if(k>length) return 0;
else{
int count=0;
p=L->next;
while(p!=NULL){
count++;
if(count==length-k+1)
break;
p=p->next;
}
printf("倒数第k个结点为:%d\n",p->data);
return 1;
}
}
例题2:
算法思想:
设置两个指针,分别遍历上下两个单链表,拿上图为例,str1长度为7,str2长度为5,那么就先让str1的指针先往后移动两次,这样就可以实现上下两个指针同步遍历,当两个指针p,q指向同一个结点时,那么这个结点就是共同后缀的起始位置。
typedef struct LNode{
int data;
LNode *next;
}LNode,*LinkList;
//计算单链表长度
int ListLen(LinkList L){
int length=0;
LNode *p=L->next;
while(p!=NULL){
length++;
p=p->next;
}
return length;
}
//找共同后缀
LNode *find_list(LinkList str1,LinkList str2){
Lnode *p,*q;
int m,n;
m=ListLen(str1);
n=ListLen(str2);
for(p=str1;m>n;m--) //如果str1>str2,p指针指向链表的第m-n+1个结点
p=p->next;
for(q=str2;n>m,n--) //如果str2>str1,q指向链表的第n-m+1个结点
q=q->next;
while(p->next!=NULL && p->next!=q->next){ //同步后移
p=p->next;
q=q->next;
}
return p->next; //返回共同后缀起始结点,return q->next也可以
}
时间复杂度O(len1+len2),即两个链表长度
按关键字查找:
删除操作:在带头结点的单链表L中,删除所有值为x的结点,并释放其空间,假设值为x的结点不唯一,试编写算法以实现上述操作。
typedef struct LNode{
int data;
struct LNode *next;
}LNode,*LinkList;
void deletX(LinkList L,int x){
LNode *pre=L;
LNode *p=pre->next;
while(p!=NULL){
if(p->data==x){
LNode *q=p;
p=p->next;
pre->next=p;
free(q);
}
else{
pre=p;
p=p->next;
}
}
}
插入操作:在一个关键字递增有序的单链表中插入新关键字x,需确保插入后单链表保持递增有序。
typedef struct LNode{
int data;
struct LNode *next;
}LNode,*LinkList;
void InsertX(LinkList L,int x){
LNode *pre=L;
LNode *p=pre->next;
while(p!=NULL){
if(p->data>x)
break;
else{
pre=p;
p=p->next;
}
}
LNode *q=(LNode *)malloc(sizeof(LNode));
q->data=x;
q->next=p;
pre->next=q;
}
例题:
思路:题目对空间复杂度没有要求,那么可以采用空间换时间的操作,使得时间复杂度尽可能高效:
定义n+1长度的数组,用这个数组统计每个绝对值出现的次数,当一个结点第一次出现时,保留他,并且用辅助数组记录。如果指针移动到下一个绝对值相同的元素,根据数组中的值判断当前的这个元素不是第一次出现了,那么删除结点。
typedef struct LNode{
int data;
struct LNode *next;
}LNode,*LinkList;
void func(LinkList L,int n){
LNode *pre=L;
LNode *p=pre->next;
int *q,m;
q=(int *)malloc(sizeof(int)*(n+1)); //申请n+1个位置的辅助空间
for(int i=0;i<n+1;i++)
*(q+i)=0;
while(p!=NULL){
m=p->data>0?p->data:-p->data;
if(*(q+m)==0){ //首次出现
*(q+m)==1; //保留
pre=p;
p=p->next;
}
else{//删除结点
LNode *s=p;
p=p->next;
pre-next=p;
free(s);
}
}
free(q); //释放辅助空间
}
头插法(实现原地逆置):
将带头结点的单链表原地逆置:
typedef struct LNode{
int data;
struct LNode *next;
}LNode,*LinkList;
void ListReserve(LinkList L){
//分配一个辅助头结点
LNode *head=(LNode *)malloc(sizeof(LNode));
head->next=NULL;
while(L->next!=NULL){
LNode *p=L->next;
L->next=L->next->next; //从L链表中拆下每一个结点
p->next=head->next; //头插法
head->next=p;
}
L->next=head->next;
free(head); //释放辅助头结点
}
时间复杂度O(1),空间复杂度O(1)
尾插法(保持原序):
设 C={a1,b1,a2,b2…,an,bn}为线性表,采用带头结点的单链表存放,设计一个就地算法,将其拆分为两个线性表,使得A={a1, a2,…,an },B= {bn,…, b2, b1}。
typedef struct LNode{
int data;
struct LNode *next;
}LNode,*LinkList;
//全局变量
LinkList A=NULL;
LinkList B=NULL;
void func(LinkList C){
A=(LNode *)malloc(sizeof(LNode));
A->next=NULL;
LNode *Atail=A; //tailA指向链尾
B=(LNode *)malloc(sizeof(LNode));
B->next=NULL;
int count=1;
while(C->next!=NULL){
LNode *p=C->next;
C->next=C->next->next; //将元素一个个从链表拆下来
if(count%2==1){ //奇数号结点:尾插法
tailA->next=p;
p->next=NULL;
tailA=p;
}
else{ //偶数号结点:头插法
p->next=B->next;
B->next=p;
}
count++; //计数值+1
}
}
例题:
思路:①找出链表L的中间结点,设置p,q指针,指针p每走一步,q每次走两步,当q到达链尾时,p到达中间结点。
② 将L的后半段原地逆置。
③ 从单链表前后各取一个结点,按要求重排。
void change_list(NODE *L){
NODE *p,*q,*r,*s;
p=q=L;
while(q->next!=NULL){
p=p->next;
q=q->next;
if(q->next!=NULL) q=q->next; //p走一步,q走两步
}
q=p; //q->next为后半链表的首结点
p-next=NULL;
while(q->next!=NULL){ //后半链表原地逆置
r=q->next;
q->next=q->next->next; //从后半链表依次拆下元素
r->next=p->next;
p->next=r;
}
s=L->next; //s指向前半段链表的第一个数据结点
q=p->next; //q指向后半段链表的第一个数据节点
p->next=NULL;
while(q!=NULL){ //相当于前半段不动,后半段插入到指定位置
r=q->next; //r指向后半段下一个结点
q->next=s->next;
s=q->next;
q=r;
}
}
5.二叉树
二叉树的前/中/后序遍历(只演示递归算法):
typedef struct BiTNode{
int data; //数据域
struct BiNode *lchild,*rchild; //左,右孩子
}BiTNode,*BiTree;
void PreOrder(BiTree root){
if(root==NULL) return ;
visit(root); //根
PreOrder(root->lchild);
PreOrder(root->rchild);
}
void InOrder(BiTree root){
if(root==NULL) return;
InOrder(root->lchild);
visit(root);
InOrder(root->rchild);
}
void PostOrder(BiTree root){
if(root==NULL) return;
PostOrder(root->lchild);
PostOrder(root->rchild);
visit(root);
}
二叉树的层序遍历:
基本操作:
#define MaxSize 50
// 定义二叉树的结点
typedef struct BiTNode {
int data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
// 定义队列
typedef struct {
BiTNode* data[MaxSize];
int front, rear;
} Queue;
// 初始化队列
void initQueue(Queue &Q) {
Q.front = Q.rear = 0;
}
// 判空
bool isEmpty(Queue Q) {
return Q.front == Q.rear;
}
// 入队
bool EnQueue(Queue &Q, BiTNode *x) {
if ((Q.rear + 1) % MaxSize == Q.front)
return false; // 队列满
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1) % MaxSize;
return true;
}
// 出队
bool DeQueue(Queue &Q, BiTNode *&x) {
if (isEmpty(Q))
return false; // 队列空
x = Q.data[Q.front];
Q.front = (Q.front + 1) % MaxSize;
return true;
}
层序遍历:
void LevelOrder(BiTree T){
Queue Q;
InitQueue(Q);
BiTree p;
EnQueue(Q,T);
while(!isEmpty(Q)){
DeQueue(Q,p);
visit(p); //看题目要求
if(p->lchild!=NULL)
EnQueue(Q,p->lchild);
if(p->rchild!=NULL)
EnQueue(Q,p->rchild);
}
}
求二叉树的高度:
方法1:定义int型变量n,用来记录当前访问的结点T在第几层,自上而下地,子树的高度就是n+1
typedef struct BiTNode{
int data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
int height=0; //二叉树高度
void PreOrder(BiTree T,int n){
if(T==NULL) return;
//访问根节点
if(n>height) height=n;
PreOrder(T->lchild,n+1); //遍历左子树
PreOrder(T->rchild,n+1); //遍历右子树
}
方法2:后序遍历左子树,返回左子树高度,后序遍历右子树,返回右子树高度,那么二叉树高度为max{左子树高度,右子树高度}+1
typedef struct BiTNode{
int data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
PostOrder(BiTree T){
if(T==NULL) return 0; //返回高度:0
int left=PostOrder(T->lchild);
int right=PostOrder(T->rchild);
if(l>r) return left+1;
else return right+1;
}
求二叉树的宽度:
#define Max 50
typedef struct BiTNode{
int data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
int width[Max];
//定义width数组,用来表示第i层的结点总数width[i]
void PreOrder(BiTree T,int level){
if(T==NULL) return;
width[level]++; //累加该层的结点
PreOrder(T,level+1);
PreOrder(T,level+1);
}
void treeWidth(BiTree T){
for(int i=0;i<Max;i++)
width[i]=0;
PreOrder(T,0); //先序遍历二叉树
int maxWidth=0;
for(int i=0;i<Max;i++){
if(width[i]>maxWidth)
maxWidth=width[i];
}
printf("树的宽度是%d",maxWidth);
}
求二叉树的带权路径长度(WPL):
一个结点的带权路径长度:
weight*这个结点到根节点的路径长度(即这个结点的层数)
typedef struct BiTNode{
int weight;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
int WPL=0;
//n用于表示T属于第几层
void PreOrder(BiTree T,int n){
if(T==NULL) return;
//如果遇到叶结点,那么就将其WPL累加到全局变量WPL中
if(T->lchild==NULL && T->rchild==NULL)
WPL+=T->weight*n;
PreOrder(T->lchild,n+1);
PreOrder(T->rchild,n+1);
}
//调用PreOrder(root,0)得到的WPL值就是这个二叉树的WPL
判断二叉树是否为二叉排序树:
遍历每个结点,使每个结点都满足:大于左孩子,且小于右孩子。只有这个条件是不够的,看下面这个例子,每个结点都满足左孩子<根结点<右孩子,但他不是二叉排序树,因为10的左子树中,11>10。
可以考虑另一种思路:
采用中序遍历,访问顺序为:左子树-->根-->右子树,只要保证中序遍历的序列是递增的,就能保证这是一棵二叉排序树。
int temp= MIN_INT; //MIN_INT:最小int值,temp用来表示已访问过的最大结点值
bool isBST=true; //刚开始默认树为二叉排序树
void InOrder(BiTree T){
if(T==NULL) return;
InOrder(T->lchild);
if(T->data>=temp) temp=T->data;
else isBST=false;
InOrder(T->rchild);
}
//调用这个函数,如果得到的结果中isBTS=false,表明这棵树不是一棵二叉排序树,否则是一棵二叉排序树
判断二叉树是否为平衡二叉树:
对于一棵二叉树,任何一个结点的左子树,右子树高度之差的绝对值不超过1,则该二叉树为平衡二叉树。
typedef BiTNode{
int data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
bool isBalance=true; //默认这棵树为平衡二叉树
int PostOrder(BiTree T){
if(T==NULL) return;
int left=PostOrder(T->lchild);
int right=PostOrder(T->rchild);
if(left-right>1) isBalance=false;
if(left-right<-1) isBalance=false;
//树的深度=max{左子树高度,右子树高度}+1
if(left>right) return left+1;
else return right+1;
}
//调用这个函数得到的结果中,isBalance=false表示这棵树不是平衡二叉树,否则是平衡二叉树
判断二叉树是否为完全二叉树:
对于完全二叉树,若某个结点的左右孩子不满,这个孩子之后的所有结点都是叶子结点,这个左右孩子不满的结点只有两种可能:
① 左右孩子都没有
② 只有左孩子,没有右孩子
若这个结点有右孩子没有左孩子,那么这棵树一定不是完全二叉树。
完全二叉树的判断,基于层序遍历进行的。
typedef BiTNode{
int data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
bool isComplete=true; //默认是完全二叉树
bool flag=false; //flag=true,表示层序遍历时出现过叶子或只有左孩子的分支结点
void visit(BiTree p){
if(p->lchild==NULL && p->rchild==NULL) flag=true;
if(p->lchild==NULL && p->rchild!=NULL) isComplete=false;
if(p->lchild!=NULL && p->rchild==NULL){
//如果这个结点有左孩子,没有右孩子,但在访问这个结点之前有访问过叶子或只有左孩子的结点
//那么这棵树一定不是完全二叉树
if(flag) isComplete=false;
flag=true;
}
if(p->lchild!=NULL && p->rchild!=NULL)
if(flag) isComplete=false;
}
//对该二叉树进行层序遍历
void LevelOrder(BiTree T){
Queue Q;
InitQueue(Q);
BiTree p;
EnQueue(Q,T);
while(!isEmpty(Q)){
DeQueue(Q,p);
visit(p);
if(p->lchild!=NULL)
EnQueue(Q,p->lchild);
if(p->rchild!=NULL)
EnQueue(Q,p->rchild);
}
}
//最后返回的结果中,isComplete=false则这棵树不是完全二叉树,否则这棵树是一棵完全二叉树