数据结构----考研必备算法题

目录

1.快速排序算法

2.利用划分函数思想解题

3.归并排序

4.单链表

按位序查找:

按关键字查找:

5.二叉树

二叉树的前/中/后序遍历(只演示递归算法):

二叉树的层序遍历:

求二叉树的高度:

求二叉树的宽度:

求二叉树的带权路径长度(WPL):

判断二叉树是否为二叉排序树:

判断二叉树是否为平衡二叉树:

判断二叉树是否为完全二叉树:


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;
}

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则这棵树不是完全二叉树,否则这棵树是一棵完全二叉树
  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值