数据结构(二)栈/队列和二叉树/堆

目录

1.栈

2.队列

3. 二叉树

4.堆


1.栈:

1.1概念介绍:

只允许一端进行数据插入和删除, 进行数据插入删除的叫栈顶, 另外一段就是栈底.

特点是先入数据后出来, 后入数据先出来.(先入后出)

1.2 栈的实现:

(1) 栈结构:

栈可以使用链表或者数组, 但是一般使用数组的.

cpp 复制代码
typedef int STDataType;

typedef struct Stack
{
    STDataType* a;
    int top;
    int capacity;
}Stack;
(2) 初始化:

创建数组a以及top和capacity置空.

cpp 复制代码
void StackInit(Stack* ps)
{
    assert(ps);

    ps->a = (STDataType*)malloc(sizeof(STDataType));
    ps->top = 0;
    ps->capacity = 0;
}
(3) 销毁:
cpp 复制代码
void StackDestroy(Stack* ps)
{
    assert(ps);

    free(ps->a);
    ps->a = nullptr;
    ps->top = ps->capacity = 0;
}
(4) 入栈:

如果栈满, 需要扩容二倍, 再插入数据.

cpp 复制代码
void StackPush(Stack* ps, STDataType data)
{
    assert(ps);

    if(ps->top == ps->capacity)
    {
        STDataType* tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) * ps->capacity * 2);
        if(tmp == nullptr)
        {
            printf("realloc fail!\n");
            exit(-1);
        }
        ps->a = tmp;
        ps->capacity *= 2;
    }
    ps->a[ps->top] = data;
    ps->top++;
}
(5) 出栈:

判断栈不为空的情况下, 再将top--就是出栈.

cpp 复制代码
bool StackEmpty(Stack* ps)
{
    assert(ps);
    return ps->top == 0;
}

void StackPop(Stack* ps)
{
    assert(ps);
    assert(!StackEmpty(ps));

    ps->top--;
}
(6) 获取栈顶元素:

先判断栈不为空, 然后再从栈中查找栈顶元素出栈即可.

cpp 复制代码
STDataType StackTop(Stack* ps)
{
    assert(ps);
    assert(!StackEmpty(ps));

    return ps->a[ps->top-1];
}
(7) 获取栈中有效元素个数:

栈中top字段恰好也记录了元素个数的.

cpp 复制代码
int StackSize(Stack* ps)
{
    assert(ps);

    return ps->top;
}

2.队列:

2.1 概念介绍:

只允许一边插入数据, 另外一边出数据. 特点是先进去的数据可以先出掉, 但是后进入的数据后面出掉(先入先出); 进数据是队尾, 出数据是队头. 而且一般使用链表结构来封装队列

2.2 实现:

(1) 结构:

结构和单链表一致. 由于出栈入栈比较频繁使用链表比数组效率高些. 由于队头队尾要记录一下使用到Queue封装一下.

cpp 复制代码
typedef int QDataType;
typedef struct QListNode
{
    QDataType data;
    QListNode* next;
}QListNode;


typedef struct Queue
{
    QListNode* head;
    QListNode* tail;
};
(2) 初始化:

起始时候tail和head都是空的.

cpp 复制代码
void QueueInit(Queue* q)
{
    assert(q);
    q->head = nullptr;
    q->tail = nullptr;
}
(3) 销毁:

因为是链表, for循环遍历链表的方式进行删除.

cpp 复制代码
void QueueDestroy(Queue* q)
{
    assert(q);
    QListNode* cur = q->head;
    while(cur)
    {
        QListNode* next = cur->next;
        free(cur);
        cur = next;
    }
    q->head = q->tail = nullptr;
}
(4) 队尾入队列:

先创建一个新结点newnode, 如果队列为空, 头和尾结点都是newnode, 否则只要插入到尾部结点之后再修改尾结点即可.

cpp 复制代码
void QueuePush(Queue* q, QDataType data)
{
    assert(q);

    QListNode* newnode = (QListNode*)malloc(sizeof(QListNode));
    if(newnode == nullptr)
    {
        printf("malloc fail!\n");
        exit(-1);
    }
    newnode->data = data;
    newnode->next = nullptr;
    if(q->head = nullptr)
    {      
        //链表为空.
        q->head = q->tail = newnode;
    }
    else
    {
        //改变尾指针.
        q->tail->next = newnode;
        q->tail = newnode;
    }
}
(5) 队头出队列:

出队列首先要判断释放队列为空, 接着就是队列只有一个数据只有删除后, head=tail=nullptr置空操作, 否则就是改变head指针指向到下一个结点位置, 然后删除结点.

cpp 复制代码
bool QueueEmpty(Queue* q)
{
    return q->head == q->tail;
}

void QueuePop(Queue* q)
{
    assert(q);
    assert(!QueueEmpty(q));

    if(q->head->next == nullptr)
    {
        //队列中只有一个结点:
        free(q->head);
        q->head = q->tail = nullptr;
    }
    else
    {
        QListNode* next = q->head->next;
        free(q->head);
        q->head = next;
    }
}
(6) 获取队列头部/尾部元素:

先检查队列释放为空, 然后不是前面封装头尾指针, 直接使用查询data数据即可.

cpp 复制代码
QDataType QueueFront(Queue* q)
{
    assert(q);
    assert(!QueueEmpty(q));

    return q->head->data;
}

QDataType QueueBack(Queue* q)
{
    assert(q);
    assert(!QueueEmpty(q));

    return q->tail->data;
}
(7) 获取队列中有效元素个数:

和链表一样队列遍历一边即可.

cpp 复制代码
int QueueSize(Queue* q)
{
    assert(q);

    QListNode* cur = q->head;
    int count = 0;

    while(cur)
    {
        count++;
        cur = cur->next;
    }
    return count;
}

2.3 循环队列:

这里不详细讲解在生产者消费者中讲解. 如果head==tail就是空队列, 但是tail+1 = head就是满的队列. 计算内部有效元素个数 = (tail - head + 队列长度) %队列长度.

3. 二叉树:

3.1 树的概念:

非线性的数据结构, 有限结点组成的具有层次的关系集合, 而且树是递归定义的; 有点像倒过来的树. 而且子树之间不能有交集, 要不然就不是树型结构了.

(1) 结点的度: 一个结点拥有结点的个数; 例如上面1的度就是2;

(2) 叶子结点(终端结点): 度为0的结点; 例如8, 9, 6, 7结点;

(3) 分支结点: 度不为0的结点; 例如4, 2, 3结点;

(4) 父节点/子节点: 拥有孩子的结点就是父节点, 孩子结点就是子节点;

(5) 树的度: 最大结点的度就是树的度;

(6) 树的高度: 树结点的最大层数;

(7) 森林: 互不相交结点的集合就是森林;

3.2 结构:

一般使用的就是孩子兄弟表示法; 因为既要保存值还要保存结点和结点之间的关系;

firstchild是第一个孩子结点, nextBrother是兄弟结点;

cpp 复制代码
typedef int DataType;
struct Node
{
    struct Node* firstchild;
    struct Node* nextBrother;
    DataType data;
};

3.3 二叉树概念:

二叉树是结点的有限集合, 一种情况是空, 另外就是根节点加上左右子树.

(1) 二叉树的度不能大于2;

(2) 二叉树有左右子树之分, 不能搞错.

(3) 二叉树有五种可能: 空, 只有根结点, 只有根节点和左子树, 只有根节点和右子树, 根节点和左右结点都有.

特殊二叉树:

满二叉树: 每一层的结点都是最大值;

完全二叉树: 可能最后一层的结点不是满的最大值, 但是左结点是有的, 不可能只有右节点没有左结点.

3.4 二叉树性质:

(1) 第i层最多2^(i-1)个结点;

(2) 深度为h的二叉树最多2^h-1个结点;

(3) 度为0的结点个数为n0, 度为2的结点个数为n2, 那么n0 = n2 + 1;

(4) n个结点的满二叉树深度是: log2^(n+1);

(5) i结点双亲结点: (i-1)/2; 左孩子结点:(2i+1); 右孩子结点:(2*i+2);

3.5 二叉树存储结构:

一般二叉树都是完全二叉树比较适合数组结构, 在物理结构是数组, 但是逻辑结构是二叉树.采用下标进行排列成二叉树

也有链表结构的二叉树:

4.堆

4.1 概念:

完全二叉树按照顺序结构进行存储在一个一维数组里面就是堆, 大堆就是第一个数据(顶数据)是最大的,小堆就是顶数据是最小的.

4.2 实现:

(1) 堆的向下调整法:

这里代码是建立小堆, 传递parent位置, 那么child位置就是parent*2+1; 首先还要比较左右结点的大小, 判断child的位置好进行后续的交换位置.(这里使用了其他博主的图).

时间复杂度是O(logN); 因为向下调整法要求左右子树都是堆结构, 所以我们从下到上建堆.

这样根据数学计算建堆的时间复杂度就是O(N);

cpp 复制代码
void Swap(int* x, int* y)
{
    int tmp = *x;
    *x = *y;
    *y = tmp;
}

//建立小堆
void AdjustDown(int* a, int n, int parent)
{
    int child = parent * 2 + 1;
    while(child < n)
    {
        if(child + 1 < n && a[child+1] < a[child])
        {
            //右孩子小于左孩子;
            child++;
        }

        if(a[child] < a[parent])
        {
            //交换父节点和子结点.
            Swap(&a[parent], &a[child]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            //以及是堆了.
            break;
        }
    }
}

//从最后一个父节点开始进行调整.
    for(int i = (n-1-1) / 2; i >= 0; i--)
    {
        AdjustDown(p->a, p->size, i);
    }
    return 0;
(2) 堆的向上调整法:

用于插入数据进行调整; 这里同样是小堆, child的值小于parent的值就进行交换, 不断改变child和parent的位置就可以走完一条路径.

cpp 复制代码
void AdjustUp(DataType* a, int child)
{
    int parent = (child-1) / 2;
    while(child > 0)
    {
        if(a[child] < a[parent])
        {
            Swap(&a[parent], &a[child]);
            child = parent;
            parent = (child - 1) / 2;
        }
        else
        {
            break;
        }
    }
}
(3) 初始化堆:

先开辟n个空间, 再将a数据拷贝新空间里面, 再进行向下调整建堆.

cpp 复制代码
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
    assert(hp);

    //开辟n个空间;
    HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType )* n);
    if(tmp == nullptr)
    {
        printf("malloc fail!\n");
        exit(-1);
    }
    hp->a = tmp;
    //将a数据拷贝到hp->a里面;
    memcpy(hp->a, a, sizeof(HPDataType)*n);
    hp->size = n;
    hp->capacity = n;

    for(int i = (hp->size-1-1) / 2; i >= 0; i--)
    {
        //建立小堆;向下调整建堆.
        AdjustDown(hp->a, hp->size, i);
    }
}
(4) 堆销毁:
cpp 复制代码
void HeapDestory(Heap* hp)
{
    assert(hp);
    free(hp->a);
    hp->a = nullptr;
    hp->size = hp->capacity = 0;
}
(5) 堆的插入:

先要检查是否需要扩容, 扩容把原数据进行拷贝, 然后插入数据, 维持堆结构进行向上调整法.

cpp 复制代码
void HeapPush(Heap* hp, HPDataType x)
{
    assert(hp);
    if(hp->size == hp->capacity)
    {
        HPDataType* tmp = (HPDataType*)realloc(hp->a, 2 * hp->capacity * sizeof(HPDataType));
        if(tmp == nullptr)
        {
            printf("realloc fail!\n");
            exit(-1);
        }
        hp->a = tmp;
        hp->capacity *= 2;
    }
    hp->a[hp->size] = x;
    hp->size++;

    //插入数据后需要保持堆结构采用向上调整法:
    AdjustUp(hp->a, hp->size-1);
}
(6) 堆的删除:

删除只能删除堆顶元素, 因为删除任意元素都可能打乱原本的结构, 所以交换堆顶和堆底元素, 然后删除堆顶元素, hp->size--即可, 然后再进行向下调整即可.

cpp 复制代码
void HeapPop(Heap* hp)
{
    assert(hp);
    assert(!HeapEmpty(hp));

    //交换堆顶和堆底元素
    Swap(&hp->a[0], &hp->a[hp->size-1]);
    hp->size--;

    AdjustDown(hp->a, hp->size, 0);
}
(7) 获取堆顶的数据:
cpp 复制代码
HPDataType HeapTop(Heap* hp)
{
    assert(hp);
    return hp->a[0];
}
(8) 获取堆的数据个数:
cpp 复制代码
int HeapSize(Heap* hp)
{
    assert(hp);
    return hp->size;
}

4.2 topk问题:

(1) 问题描述:

给一个数组找到最大的k个数; 例如: 数组:[1, 3, 4, 2, 5 ,7] ,找到最大的三个数就是

[4, 5, 7]

(2) 解决方案(一):

使用堆结构, 建立小堆, 然后再进行降序排序, 取出前k个即可. 可以思考一下降序我们就需要建立小堆 (因为大的已经在堆顶排列好), 升序建立大堆; 这样时间复杂度就是

O(N + NlogN).

cpp 复制代码
int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize)
{
    *returnSize = k;
    //建立小堆.
    for(int i = (arrSize-1-1) / 2; i >= 0; i--)
    {
        AdjustDown(arr, arrSize, i);
    }

    //排降序;
    int end = arrSize - 1;
    while(end > 0)
    {
        Swap(&arr[0], &arr[end]);
        AdjustDown(arr, end, 0);
        end--;
    }

    int* retArr = (int*)malloc(sizeof(int) * k);
    for(int i = 0; i < k; i++)
    {
        retArr[i] = arr[i];
    }
    return retArr;
}
(3) 解决方案(二):

采用建大堆的方法, 然后取出前k个大的数据, 先取堆顶元素, 然后进行向下调整法, 在得到次大的数取出来, 一直重复取得前k个大的数, 这样时间复杂度就是O(N + KlogN) ->O(N+logN).

cpp 复制代码
//建立大堆
void AdjustDown(int* a, int n, int parent)
{
    int child = parent * 2 + 1;
    while(child < n)
    {
        if(child + 1 < n && a[child+1] > a[child])
        {
            //右孩子小于左孩子;
            child++;
        }

        if(a[child] > a[parent])
        {
            //交换父节点和子结点.
            Swap(&a[parent], &a[child]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            //以及是堆了.
            break;
        }
    }
}


int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize)
{
    *returnSize = k;
    for(int i = (arrSize-1-1) / 2; i >= 0; i--)
    {
        AdjustDown(arr, arrSize, i);
    }

    //将最大k个数保持到数组;
    int* retArr = (int*)malloc(sizeof(int) * k);
    int end = arrSize - 1;
    for(int i = 0; i < k; i++)
    {
        retArr[i] = arr[0];
        Swap(&arr[0], &arr[end]);
        AdjustDown(arr, end, 0);
        end--;
    }
    return retArr;
}
(4) 解决方案(三):

如果数据很大, 有1001亿个呢? 还能用吗? 有没有其他方法?

先将前k个数进行建立小堆, 然后后面n-k个数就是一个个和堆顶元素进行比较, 如果比堆顶元素大, 就交换, 在进行调整选出其中最小的进行下一次的交换(就是被比下去了).这样时间复杂度就是O(K + NlogK) ---> O(N);

cpp 复制代码
//建立小堆
void AdjustDown(int* a, int n, int parent)
{
    int child = parent * 2 + 1;
    while(child < n)
    {
        if(child + 1 < n && a[child+1] < a[child])
        {
            //右孩子小于左孩子;
            child++;
        }

        if(a[child] < a[parent])
        {
            //交换父节点和子结点.
            Swap(&a[parent], &a[child]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            //以及是堆了.
            break;
        }
    }
}


int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize)
{
    *returnSize = k;
    if(k == 0)
        return nullptr;

    int* retArr = (int*)malloc(sizeof(int) * k);
    for(int i = 0; i < k; i++)
    {
        retArr[i] = arr[i];
    }

    //前k个数建小堆;
    for(int i = (k - 1 - 1) / 2; i >= 0; i--)
    {
        AdjustDown(retArr, k, i);
    }

    //将后n-k个数据每一个和retArr[0]进行比较, 
    //然后每次在k个里面选出最小和n-k个数据进行比较交换调整.
    for(int i = k; i < arrSize; i++)
    {
        if(arr[i] > retArr[0])
        {
            retArr[0] = arr[i];
        }
        AdjustDown(retArr, k, 0);
    }
    return retArr;
}
相关推荐
计算机小混子11 分钟前
C++实现设计模式---外观模式 (Facade)
c++·设计模式·外观模式
王景程24 分钟前
Java冒泡排序算法之:变种版
java·数据结构·算法
简简单单一天吃六顿1 小时前
C语言重点回顾(持续更新中~)
c语言
迂幵myself1 小时前
13-1类与对象
开发语言·c++·算法
大米~¥2 小时前
VSCode的配置与使用(C/C++)
c++·ide·vscode
zc.ovo2 小时前
寒假康复训练2 edu111(A-C)
c语言·数据结构·算法
雾月553 小时前
LeetCode1170 比较字符串最小字母出现频次
数据结构·算法
m0_dawn3 小时前
算法竞赛(蓝桥杯)贪心算法1——数塔问题
数据结构·python·算法·职场和发展·蓝桥杯
许白掰4 小时前
C语言学习——指针与数组,指针与函数,指针与堆空间
c语言·学习·算法
张小小大智慧5 小时前
排序算法(C语言版)
数据结构·算法·排序算法