面试——数据结构

本文详细解析了指针内存错误问题,特别是针对链式存储的指针使用注意事项,并介绍了栈的链式存储结构及其操作方法,包括初始化、压栈、出栈和特殊实现技巧。此外,还探讨了利用两栈实现队列和两队列实现栈的技术,以及快速排序、冒泡排序等排序算法的基本原理和实现。最后,文章深入浅出地讲解了二叉树的构建、遍历方式以及图的深度优先和广度优先搜索策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、指针内存错误问题(有关链式存储)

在此之前说一个当时写此实现的一个指针内存错误
一个指针,不管定义成什么类型,当要去取它的内存模型中的某个东西时,这个指针所指向的内存必须存在,也就是之前给其所指向的内存分配了空间。
当定义:

    int *p;
    *p = 5;  //执行这样的操作是错误的,这是简单的,
//然而复杂的是如下的,定义一个结构体指针

//节点元素
typedef struct node1
{
    int data;
    struct node1 * next;
}node;

//栈节点
typedef struct stack
{
    node * btn,*top;
}stack;

stack *st;
//以下的两个操作都是错误的,将导致segment fault!
st->btn = st->top = NULL;
s->btn = s->top = (node*)malloc(sizeof(node));

因为stack *st;只声明了一个st的指针,系统给其只分配了固定大小(4字节地址大小)的内存,它所指向的内存区域还未分配内存。
【如图分析】
这里写图片描述

//简单的例子
typedef struct test
{
    int a,b;
}test;

test t;  //此处系统给其分配了一个test结构体大小的内存
test *p; //此处系统给其分配了一个指针大小的内存(大部分固定为4字节)
t.a = 5;  
p->a = 10;  //执行错误

2、栈的链式存储结构

/*
* 栈的链式存储结构
* top栈顶指针永远指向栈顶元素的下一个位置
* btn不变,指向栈底
* 栈空的判断为,栈顶 == 栈底
* 初始化时,让栈顶和栈底一起指向一块内存(node)
* 入栈时:总是先将元素放置在栈顶top指向的位置,然后再后移top
* 出栈时:总是先前移top,再取top位置的值
*/

//节点元素
typedef struct node1
{
    int data;
    struct node1 * next;
}node;

//栈节点
typedef struct stack
{
    node * btn,*top;
}stack;

int init_stack(stack *s)
{
    s->btn = s->top = (node*)malloc(sizeof(node));
    if(!s->btn)
        return 0;
    s->btn->next = s->top->next = NULL;
    return 1;
}

//压栈
void push(stack *s, int data)
{
    node *q;
    q = (node *)malloc(sizeof(node));
    if(!q)
        return;
    q->next = NULL;

    s->top->data = data;
    s->top->next = q;
    s->top = q;
}

//出栈
int pop(stack *s)
{
    node *q,*t;
    int ret = -1;

    if(s->top == s->btn)
    {
        printf("empty stack!\n");
    }
    else
    {
        t = s->top;
        q = s->btn;
        while(q->next != t)  //寻找top前一个位置的指针
        {
            q=q->next;
        }
        ret = q->data;  //取top前一位置的值
        s->top = q;     
        //s->top = NULL; 此处应该是s->top->next = NULL;

        free(t);
    }

    return ret;    
}

void print_stack(stack *s)
{
    node *q;

    for(q=s->btn; q!=s->top;q=q->next)
    {
        printf("%d\n",q->data);
    }
}

int main()
{
    stack *st;
    st = (stack *)malloc(sizeof(stack));

    init_stack(st);

    int i;
    for(i=0;i<5;i++)
    {
        push(st,i);
    }
    print_stack(st);
    printf("==========\n");
    printf("%d\n\n",pop(st));
    printf("%d\n\n",pop(st));
    printf("%d\n\n",pop(st));
    printf("%d\n\n",pop(st));
    printf("%d\n\n",pop(st));
    printf("%d\n\n",pop(st));
    printf("%d\n\n",pop(st));    
    print_stack(st);

    return 1;
}

3、用两栈实现队列、两队列实现栈

两栈实现队列:
【思路】:
若s2空, 则弹出s1数据到s2中,
弹出s2中的数据
若s2不空, 则直接 弹出s2中的数据

//入队
void en_que(int data)
{
    push(s1,data);
}

//s1专属进队列
//自己的思路是:将s1入栈到s2,从s2中出一个数后,又将s2入栈到s1,非常耗时!!
int de_que()
{
    int ret = -1;

    while(s1->top != s1->btn)
        push(s2,pop(s1));
    if(s2->top == s2->btn)
    {
        printf("queue is empty\n");
        return ret;
    }
    ret = pop(s2);
    while(s2->top != s2->btn)
        push(s1,pop(s2));

    return ret;
}

//s1专属进队列
//若s2空,则弹出s1数据到s2中,出s2中的数据
//若s2不空,则直接弹出s2中的数据
int de_que_ex()
{
    if(s2->top == s2->btn)
    {
        while(s1->top != s1->btn)
            push(s2,pop(s1));
    }

    if(s2->top == s2->btn)
    {
        printf("queue is empty\n");
        return -1;
    }
    else
    {
        return pop(s2);
    }
}

两队列实现栈:
在C++ STL中有双向队列deque,当单向的来用就行,设有两个队列A和B,栈的push操作,直接push到A的队尾就行了。栈的pop操作时,将A中的队列依次取出放到B中,取到最后一个时,最后一个不要放到B中,直接删掉,再将B中的值依次放回A中。栈的top操作时,将A中的队列依次取出放到B中,取到最后一个时,将最后一个值记录下来,再将最后一个值放到B中,再将B中的值依次放回到A中。

#include<iostream>  
#include <deque>  
using namespace std;  
template<class T> class Mystack  
{  
public:  
    Mystack(){}  
    ~Mystack(){}  
    void push(T t);  
    T top();  
    void pop();  
private:  
    deque<T> A;  
    deque<T> B;  

};  

template<class T> void  Mystack<T>::push(T t)  
{  
    A.push_back(t);  
}  
template<class T> T Mystack<T>::top()  
{  
    while(A.size()>1)  
    {  
        B.push_back(A.front());  
        A.pop_front();  
    }  

    T tmp=A.front();  
    B.push_back(A.front());  
    A.pop_front();  

    while(B.size()!=0)  
    {  
        A.push_back(B.front());  
        B.pop_front();  
    }  
    return tmp;  
}  
template<class T> void Mystack<T>::pop()  
{  
    while(A.size()>1)  
    {  
        B.push_back(A.front());  
        A.pop_front();  
    }  
    A.pop_front();  
    while(B.size()!=0)  
    {  
        A.push_back(B.front());  
        B.pop_front();  
    }   
}  

4、快速排序

【重点】
一次快速排序时,从两边往内移动的两个指针low/high,只需比较low < high
当low==high时,正好比完,而哨兵值(中间值)就应该放在low/high处,记得将low/high返回

//一趟快速排序过程
int quick_sort(int *b, int low, int high)
{
    int tmp = b[low];

    while(low < high)
    {
        while(low < high && b[high] >= tmp)
            high --;
        if(low < high)
            b[low++] = b[high];
        while(low < high && b[low] <= tmp)
            low ++;
        if(low < high)
            b[high--] = b[low];        
    }
    b[high] = tmp;   //重点1

    return high;     //重点2
}

//递归快速排序
void Q_sort(int *b, int low, int high)
{
    int mid;
    if(low < high)  //重点3
    {
        mid = quick_sort(b,low,high);  //重点4
        Q_sort(b,0,mid-1);
        Q_sort(b,mid+1,high);
    }
}

void q_sort(int *b, int n)
{
    Q_sort(b,0,n-1);
}

5、冒泡排序

【算法思路】
用图表达:
这里写图片描述

//冒泡排序 从小到大的顺序
void bubule(int *b, int n)
{
    int i,j;

    for(i=n-1;i>0;i--)
    {
        for(j=0;j<i;j++)  
        {
            //if(b[j] > b[i])  //找到一个最大的给b[i],即是从小到大的顺序
            if(b[j] > b[j+1])  //从头到i依次比较相邻数的大小
            {
                swap2(&b[j],&b[i]);
            }
        }
    }
}

//冒泡排序,带判断标志
void bubule(int *b, int n)
{
    int i,j,is_order = 1;

    for(i=n-1;i>0;i--)
    {        
        is_order = 1;  //每次循环比较之前,设置标志位
        for(j=0;j<i;j++) 
        {
            if(b[j] > b[j+1])
            {
                swap2(&b[j],&b[j+1]);
                is_order = 0;    //一旦有交换,则清除标志位
            }
        }
        if(is_order)
            break;
    }
}

6、直接插入排序

【算法思路】:把一个数,插入到一个已经有序的表中。插入时,在有序表中从后往前寻找插入点,顺便移动数据。

//直接插入排序
void insert_sort(int *b, int n)
{
    int i,j,tmp;
    for(i=1; i<n; i++)
    {
        tmp = b[i];
        for(j=i-1; j>=0 && tmp<b[j]; j--)
        {
                b[j+1] = b[j];
        }
        b[j+1] = tmp;
    }
}

7、二路插入排序

【算法思路】:就是在直接插入的基础上,寻找插入点的时候利用二分查找的办法。

//二路插入排序
void twoway_insert_sort(int *b, int n)
{
    int i,j,tmp,low,high,mid;
    for(i=1; i<n; i++)
    {
        tmp = b[i];
        //二路查找
        low = 0;
        high = i-1;
        while(low <= high)
        {
            mid = (low+high)/2;
            if(tmp > b[mid])
                low = mid+1;
            else
                high = mid-1;
        }
        for(j=i-1;j>=low;--j)    //最终当low > high的时候,我们需要从i~low的位置移动,即插入的位置在low/high+1的位置上。
            b[j+1] = b[j];
        b[j+1] = tmp;   //b[high+1] = tmp;  b[low] = tmp;都可以
    }
}

8、希尔插入排序

【算法思路】:通过一个增量dk,将数组分成S = { k,k+dk,k+2dk,k+3dk…. }(k的范围是0~dk-1),这样子就是将S这个序列进行直接插入排序了。然后随着增量dk的减小,最终减到1,一次直接插入排序排好。
这里写图片描述

//一趟希尔排序shell sort
void shellsort(int *b, int n, int dk)
{
    int i,j,k,tmp;

    for(i=dk; i<n; ++i)   //①
    {
        if(b[i] < b[i-dk])
        {
            tmp = b[i];
            for(j=i-dk; j>=0 && b[j]>tmp; j-=dk)  //②
            {
                b[j+dk] = b[j];
            }
            b[j+dk] = tmp;     
        }
    }
}

void shell_sort(int *b, int n)
{
    int dk[] = {5,3,1};
    int i;
    for(i=0;i<3;++i)
    {
        shellsort(b,n,dk[i]);
    }
}

9、二分查找

//二路查找(查找c,返回在b中的位置)
int twoway_search(int *b, int n, int c)
{
    int low,high,mid;
    low = 0;
    high = n-1;
    while(low <= high)  //重点
    {
        mid = (low + high)/2;
        if(b[mid] == c)
            return mid;
        if(b[mid] < c)
            low = mid +1;
        else if(b[mid > c])
            high = mid -1;
    }

    return -1;  //未找到
}

10、二叉查找树(排序树、搜索树)->红黑树

节点值 < < 节点值
这里写图片描述

二叉查找树的一般性质:
1.在一棵二叉查找树上,执行查找、插入、删除等操作,的时间复杂度为O(lgn)。
因为,一棵由n个结点,随机构造的二叉查找树的高度为lgn,所以顺理成章,一般操作的执行时间为O(lgn)。
2.但若是一棵具有n个结点的线性链,则此些操作最坏情况运行时间为O(n)。

11、选择排序

选择排序的思路:
①初始状态:无序区为R[1..n],有序区为空。
②第1趟排序
在无序区*R[1..n]中选出关键字最小的记录R[k],将它与无序区的第1个记录R[1]交换*,使R[1..1]和R[2..n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
……
③第i趟排序
第i趟排序开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。

选择排序是不稳定的排序方法(比如序列[5, 5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面)。

堆排序

数组从1开始计算,0舍弃。
1、调整堆

//a为堆数组,i为需要调整的子树的根节点指针,size为数组中元素的个数
void HeapAdjust(int *a,int i,int size)  //调整堆 
{
    int lchild=2*i;       //i的左孩子节点序号 
    int rchild=2*i+1;     //i的右孩子节点序号 
    int max=i;            //记录根和其左右孩子的最大值指针
    if(i<=size/2)         //从第一个非叶子节点开始调整
    {
        if(lchild<=size && a[lchild]>a[max])
        {
            max=lchild;
        }    
        if(rchild<=size && a[rchild]>a[max])
        {
            max=rchild;
        }
        if(max!=i)
        {
            swap(a[i],a[max]);
            HeapAdjust(a,max,size);    //避免调整之后以max为父节点的子树不是堆 
        }
    }        
}

2、对一个初始的数组建立堆

//a为堆数组,size为数组中元素的个数
void BuildHeap(int *a,int size)    //建立堆 
{
    int i;
    for(i = size/2; i >= 1; i--)    //从第一个非叶子结点开始建立堆 
    {
        HeapAdjust(a,i,size);    
    }    
} 

3、堆排序:建立堆,然后交换堆顶元素和最后一个元素,从新调整堆

void HeapSort(int *a,int size) //堆排序 
{
    int i;
    BuildHeap(a,size);

    for(i=size; i>=1; i--)
    {
        //cout<<a[1]<<" ";
        swap(a[1],a[i]);          //交换堆顶和最后一个元素,即每次将剩余元素中的最大者放到最后面 
        HeapAdjust(a,1,i-1);      //重新调整堆顶节点成为大顶堆
    }
} 

12、二叉树

创建二叉树,按先序遍历创建 12 45 0 65 23 0 0 45 先序创建。

void Create_BiTree(BiTree **t)
{
    int a;
    BiTree *p;

    scanf("%d",&a);
    if(a == 0)
        *t = NULL;
    else
    {
        p = (BiTree *)malloc(sizeof(BiTree));
        if(!p)
            exit(1);
        p->data = a;

        *t = p;  //*t指向p所申请的的内存,此处将改变根节点的指针,因此必须传入根节点的指针的指针。

        Create_Tree(&(*t)->left);
        Create_Tree(&(*t)->right);
    }
}

中序遍历二叉树(非递归算法)

1:
当二叉树不空或者栈不空时,循环
{
当二叉树不空,当前节点进栈, 然后指向左子树
当二叉树空时,出栈一个节点,访问之,然后指向右子树
}

void InOrderTraverse_noRec(BiTree t)
{
    BiTree stack[100],tmp;
    int top,base;
    top = base = 0;

    while(t || top != base)
    {
        if(t)  //二叉树不空 进栈->往左走一步
        {
            stack[top++] = t;
            t = t->left;
        }
        else  //二叉树空 出栈->访问->向右走一步
        {
            t = stack[--top];
            printf("%d\n",t->data);
            t = t->right;
        }
    }
}

2:
根节点进栈
栈不空时,循环
{
得到栈顶元素,栈顶指针不空,循环,向左走到头,进栈每个节点的左孩子
出栈空节点
然后,栈不空时出栈,访问之,再入栈右孩子指针
}

void InOrderTraverse_noRec2(BiTree t)
{
    BiTree stack[100],tmp;
    int top,base;
    top = base = 0;

    stack[top++] = t;    //根节点进栈
    while(top != base)   //栈不空时,循环
    {
        while(tmp = stack[top-1])   //栈顶元素不空时,进栈所有左孩子节点
        {
            stack[top++] = tmp->left;
        }
        top--;  //出栈空节点(包括while循环进的最后一个空节点,或下面进栈的一个空的右孩子)

        if(top != base)
        {
            tmp = stack[--top];
            printf("%d\n",tmp->data);
            stack[top++] = tmp->right;
        }
    }
}

后续遍历二叉树(非递归算法)

void PostOrderTraverse(BiTree t)
{
    if(t)
    {
        PostOrderTraverse(t->left);
        PostOrderTraverse(t->right);
        printf("%d\n",t->data);
    }
}

void PostOrderTraverse_noRec(BiTree t)
{
    BiTree stack[100];
    int flag[100],top,base;
    top = base = 0;

    while(t || top != base)  //二叉树不空或者栈不空
    {
        //压栈直到左子树为空
        while(t)
        {
            stack[top++] = t;
            flag[top-1] = 0;
            t = t->left;
        }

        //栈不空并且右子树已经访问过了就该访问根节点了
        while(top != base && flag[top-1] == 1)
        {
            t = stack[--top];
            printf("%d\n",t->data);
        }

        //栈不空时取栈顶元素的右孩子
        if(top != base)
        {
            flag[top-1] = 1;
            t = stack[top-1]->right;
        }
    }
}


// 后序遍历二叉树的非递归算法
void postorder2(Bitree *t)
{
    Bitree *s[32];  // s是指针数组,数组中元素为二叉树节点的指针
    int tag[32];    // s中相对位置的元素的tag: 0或1
    int top = -1;
    while (t!=NULL || top != -1)
    {
        // 压栈直到左子树为空
        while (t != NULL)
        {
            s[++top] = t;
            tag[top] = 0;
            t = t->lchild;
        }
        // 当栈非空,并且栈顶元素tag为1时,出栈并访问
        while (top!=-1 && tag[top]==1)
        {
            printf("%c ", s[top--]->data);
        }
        // 当栈非空时,将栈顶tag置1,并指向栈顶元素的右孩子
        if (top != -1)
        {
            tag[top] = 1;
            t = s[top]->rchild;
        }
    }
}

13、图

使用深度优先搜索求简单路径

求一个顶点v到顶点s的简单路径(没有回路的路径)
例如:求a到e的简单路径
先从a出发,然后访问b,再访问c,此时记住a->b->c的这个路径,
当c又访问a时,此时c已经遍历完了,还没有找到e,因此将c出路径,
再退回到b,b的临节点都访问了,因此将b出路径。
再退回到a,访问未访问的d,将d加入路径,
然后再访问e,加入路径
已经访问到了e,输出路径:a->d->e
这里写图片描述

算法伪代码:

void DFSsearch(int v, int s, char *path)
{
    visit[v] = TRUE;  //访问第v个节点
    Append(path, v);  //将v节点加入到路径

    for(w=FirstAdjVex(v), w!=0 && !found; w=NextAdjVex(v))  //从v的第一个邻节点到最后一个邻节点
    {
        if(w == s)  //找到s
        {
            found = TRUE;
            Append(path, w);
        }
        else if(!visit[w]) //w未被访问到
        {
            DFSsearch(w,s,path);
        }
    }
    if(!found)
    {
        Delete(v,path);  //当v这个节点的所有邻节点都访问完了,还未找到,则将v退出路径
    }
}

使用广度优先搜索求最短路径

例:求顶点3到顶点5的最短路径,应该是3->1->4->
这里写图片描述

从3开始广度遍历,(广度遍历每次都是增加一个深度,需要一个队列辅助实现)
1)将链队列的节点改为双链,包含一个pre指向深度遍历时的父节点。
2)修改入队列的操作,插入新的队尾结点时,令其pre指向刚刚出队列的结点。
3)修改出队列的操作,出队列时,仅移动队头指针,而不删除。
4)当找到5时,从5开始顺着pre指针到队列头,此即其路径。
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值