数据结构初阶

文章目录

一、前言

1.1 什么是数据结构

数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。

1.2 如何学习数据结构

了解底层原理

多画图思考

多刷题

1.3 推荐书籍

《剑指offer》

《程序员代码面试指南》

二、复杂度

2.1 算法效率

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 ,因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。

时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。

在计算机发展的早期,计算机的存储容量很小,所以对空间复杂度很是在乎,但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

2.2 时间复杂度

2.2.1 时间复杂度的概念

在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。

一个算法所花费的时间与其中语句的执行次数成正比例,因此算法中的基本操作的执行次数为算法的时间复杂度,即找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。

2.2.2 大O的渐进表示法

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。

推导大O阶方法:

1、用常数1取代运行时间中的所有加法常数。

2、在修改后的运行次数函数中,只保留最高阶项。

3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

而且在实际中一般情况关注的是算法的最坏运行情况

2.3 空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。

空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。

空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。

注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。空间复杂度针对的是临时空间,例如斐波那契数列空间复杂度为O(n)

2.4 常见复杂度对比

一般复杂度算法如下:

O(1)常数时间复杂度
O(n)线性时间复杂度
O(n^2)平方时间复杂度
O(logn)对数时间复杂度
O(nlogn)线性对数时间复杂度
O(n^3)立方时间复杂度
O(2^n)指数时间复杂度
O(n!)阶乘时间复杂度

时间复杂度曲线如下:
在这里插入图片描述

2.5 复杂度oj练习

(1)消失的数字

面试题 17.04. 消失的数字 - 力扣(LeetCode)

思路一:异或(时间复杂度O(n),空间复杂度O(1))

异或特性:

任何数与自己异或都会得到0

任何数与0异或都会得到它自己

class Solution {
public:
    int missingNumber(vector<int>& nums) 
    {
        int ret=0;
        for(auto e:nums)
        {
            ret^=e;
        }
        for(int i=0;i<=nums.size();i++)
        {
            ret^=i;
        }
        return ret;
    }
};

思路二:映射(时间复杂度O(n),空间复杂度O(n))

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        vector<int> array=vector<int>(nums.size()+1,-1);
        for(auto e:nums)
        {
            array[e]=e;
        }
        for(int i=0;i<=nums.size();i++)
        {
            if(array[i]==-1)
                return i;
        }
        return -1;
    }
};

思路三:排序(以冒泡为例)(时间复杂度O(n^2),空间复杂度O(1))

思路四:等差数列公式(时间复杂度O(n),空间复杂度O(1))

(2)旋转数组

189. 轮转数组 - 力扣(LeetCode)

思路一:右旋k次(时间复杂度O(n*k),空间复杂度O(1))

class Solution {
public:
    void _rotate(vector<int>& nums)
    {
        int tmp=nums[nums.size()-1];
        for(int i=nums.size()-1;i>0;i--)
        {
            nums[i]=nums[i-1];
        }
        nums[0]=tmp;
    }
    void rotate(vector<int>& nums, int k) 
    {
        k=k%nums.size();
        while(k--)
        {
            _rotate(nums);
        }
    }
};

思路二:额外数组(时间复杂度O(n),空间复杂度O(n))

思路三:逆置(时间复杂度O(n),空间复杂度O(1))

  1. 先将整个数组逆序。
  2. 将数组的前k个元素逆序。
  3. 将数组的剩余元素逆序。
class Solution {
public:
    void reverse(vector<int>& nums, int begin, int end) 
    {
        while (begin < end) 
        {
            swap(nums[begin], nums[end]);
            begin++;
            end--;
        }
    }
    void rotate(vector<int>& nums, int k) 
    {
        int n = nums.size();
        k %= n;
        reverse(nums, 0, n - 1);
        reverse(nums, 0, k - 1);
        reverse(nums, k, n - 1);
    }
};

三、顺序表、链表

3.1 线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。

线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串等。

线性表在逻辑上是线性结构,也就说是连续的一条直线,但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

3.2 顺序表

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。

3.2.1 顺序表分类

静态顺序表:使用定长数组存储元素

动态顺序表:使用动态开辟的数组存储

3.2.2 结构

typedef int SLDataType;
// 顺序表的动态存储
typedef struct SeqList
{
 SLDataType* array; // 指向动态开辟的数组
 size_t size ; // 有效数据个数
 size_t capicity ; // 容量空间的大小
}SeqList;

3.2.3 接口实现

具体实现参考gitee仓库:https://gitee.com/JIzaodeyy/data-structure.git

// 基本增删查改接口
// 顺序表初始化
void SeqListInit(SeqList* psl, size_t capacity);
// 检查空间,如果满了,进行增容
void CheckCapacity(SeqList* psl);
// 顺序表尾插
void SeqListPushBack(SeqList* psl, SLDataType x);
// 顺序表尾删
void SeqListPopBack(SeqList* psl);
// 顺序表头插
void SeqListPushFront(SeqList* psl, SLDataType x);
// 顺序表头删
void SeqListPopFront(SeqList* psl);
// 顺序表查找
int SeqListFind(SeqList* psl, SLDataType x); 
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* psl, size_t pos);
// 顺序表销毁
void SeqListDestory(SeqList* psl);
// 顺序表打印
void SeqListPrint(SeqList* psl);

3.2.4 相关笔试题

(1)移除元素

27. 移除元素 - 力扣(LeetCode)

思路1:调用接口查询后删除

//(时间复杂度O(n^2),空间复杂度O(1))
class Solution {
public:
    int removeElement(vector<int>& nums, int val) 
    {
        while(true)
        {
            //find复杂度为O(n)
            auto pos=find(nums.begin(),nums.end(),val);
            if(pos!=nums.end())
            {
                //erase复杂度为O(n)
                nums.erase(pos);
            }
            else
            {
                return nums.size();
            }   
        }
    }
};

思路二:双指针重排数组

//(时间复杂度O(n),空间复杂度O(1))
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int i = 0;
        for (int j = 0; j < nums.size(); j++) {
            if (nums[j] != val) 
            {
                nums[i] = nums[j];
                i++;
            }
        }
        return i;
    }
};
(2)删除排序数组中的重复项

26. 删除有序数组中的重复项 - 力扣(LeetCode)

思路:双指针重排数组

//(时间复杂度O(n),空间复杂度O(1))
class Solution {
public:
    int removeDuplicates(vector<int>& nums) 
    {
        int j=0;
        int i=0;
        for(i;i<nums.size()-1;i++)
        {
            if(nums[i]!=nums[i+1])
            {
                nums[j]=nums[i];
                j++;
            }
        }
        nums[j]=nums[i];
        return j+1;
    }
};
(3)合并两个有序数组

88. 合并两个有序数组 - 力扣(LeetCode)

//(时间复杂度O(n+m),空间复杂度O(1))
class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) 
    {
        int pos = m + n - 1;
        while (m > 0 && n > 0) 
        {
            if (nums1[m - 1] > nums2[n - 1]) 
            {
                nums1[pos] = nums1[m - 1];
                m--;
            }   
        else 
        {
            nums1[pos] = nums2[n - 1];
            n--;
        }
        pos--;
    }
    while (n > 0) 
    {
        nums1[pos] = nums2[n - 1];
        n--;
        pos--;
    }
    }
};

3.2.5 优缺点

(1)优点

连续物理空间,方便下标访问

(2)缺点

中间/头部的插入删除,时间复杂度为O(N)

增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗。

增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

3.3 链表

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。

3.3.1 链表分类

单向和双向

带头和不带头

循环和非循环

(1)无头单向非循环链表

结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结 构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

(2)带头双向循环链表

结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

(3)链表带头节点的作用

避免空链表特殊处理:在不带头节点的链表中,对空链表的操作需要特殊处理。例如,当链表为空时,需要修改头指针的值。带头节点的链表可以避免这种特殊处理,因为头节点始终存在。

提供一个固定的引用点:无论链表中的数据如何变化,头节点始终保持不变,这为某些操作提供了一个固定的引用点。例如带头时,在传参时,可以使用头节点,而不需要使用二级指针。

3.3.2 结构

(1)无头单向非循环链表
typedef int SLTDateType;
typedef struct SListNode
{
    SLTDateType data;
    struct SListNode* next;
}SListNode;
(2)带头双向循环链表
typedef int LTDataType;
typedef struct ListNode
{
 LTDataType _data;
 struct ListNode* next;
 struct ListNode* prev;
}ListNode;

3.3.3 接口实现

(1)无头单向非循环链表

具体代码参考gitee仓库:https://gitee.com/JIzaodeyy/data-structure.git

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
(2)带头双向循环链表

具体代码参考gitee仓库:https://gitee.com/JIzaodeyy/data-structure.git

// 创建返回链表的头结点.
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* plist);
// 双向链表打印
void ListPrint(ListNode* plist);
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* plist);
// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* plist);
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

3.3.4 相关笔试题

(1)移除链表元素

203. 移除链表元素 - 力扣(LeetCode)

思路:带头结点处理

不需要考虑空链表,以及头节点删除的特殊处理

//(时间复杂度O(n),空间复杂度O(1))
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) 
    {
        ListNode *newhead=new ListNode(0);
        newhead->next=head;
        ListNode *pre=newhead;
        while(head)
        {
            if(head->val==val)
            {
                pre->next=head->next;
                head=head->next;
            }
            else
            {
                pre=head;
                head=head->next;
            }
        }
        return newhead->next;
    }
};
(2)反转链表

206. 反转链表 - 力扣(LeetCode)

思路1:迭代

头插至newHead,注意修改newHead时,是修改这个指针,涉及二级指针

//(时间复杂度O(n),空间复杂度O(1))
class Solution {
public:
    void PushFront(ListNode**newHead,ListNode*node)
    {
        node->next=*newHead;
        *newHead=node;
    }
    ListNode* reverseList(ListNode* head) 
    {
        ListNode*newHead=nullptr;
        while(head)
        {
            ListNode* next = head->next;
            PushFront(&newHead,head);
            head=next;
        }
        return newHead;
    }
};

思路2:递归

函数的功能:nk的下一个指向nk+1,即nk.next.next=nk

结束条件:链表为空或链表只剩一个节点

等价关系:反转链表相当于反转第一个节点,和后面已反转的节点

//(时间复杂度O(n),空间复杂度O(n))
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (!head||!head->next) {
            return head;
        }
        ListNode* newHead = reverseList(head->next);
        head->next->next = head;
        head->next = nullptr;
        return newHead;
    }
};
(3)链表的中间结点

876. 链表的中间结点 - 力扣(LeetCode)

思路:快慢指针

(时间复杂度O(n),空间复杂度O(1))
class Solution {
public:
    ListNode* middleNode(ListNode* head) 
    {
        ListNode*slow=head;
        ListNode*fast=head;
        while(fast&&fast->next)
        {
            slow=slow->next;
            fast=fast->next->next;
        }
        return slow;
    }
};
(4)返回倒数第k个结点

链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)

思路:快慢指针

//(时间复杂度O(n),空间复杂度O(1))
class Solution {
public:
    ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) 
	{
		if(pListHead==nullptr)
		return nullptr;
		ListNode* cur=pListHead;
		int n=0;
		while(cur)
		{
			cur=cur->next;
			n++;
		}
		if(k>n)
		return nullptr;
		ListNode*slow=pListHead;
		ListNode*fast=pListHead;
		while(k--)
		{
			fast=fast->next;
		}
		while(fast)
		{
			fast=fast->next;
			slow=slow->next;
		}
		return slow;
    }
};
(5)合并两个有序链表

21. 合并两个有序链表 - 力扣(LeetCode)

思路1:双指针迭代

//(时间复杂度O(n+m),空间复杂度O(1))
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) 
    {
        ListNode* newHead=new ListNode(0);
        ListNode*newTail=newHead;
        ListNode*ptr1=list1;    
        ListNode*ptr2=list2;
        while(ptr1&&ptr2)
        {
            if(ptr1->val<ptr2->val)
            {
                //pushback
                newTail->next=ptr1;
                newTail=ptr1;
                ptr1=ptr1->next;
            }
            else
            {
                //pushback
                newTail->next=ptr2;
                newTail=ptr2;
                ptr2=ptr2->next;
            }
        }
        if(ptr1)
        {
            newTail->next=ptr1;
        }
        if(ptr2)
        {
            newTail->next=ptr2;
        }
        return newHead->next;    
    }
};

思路2:递归

函数的功能:头插

结束条件:其中一个链表为空则返回另一个链表

等价关系:两个链表较小的值+已经合并的有序链表

//(时间复杂度O(n+m),空间复杂度O(n+m))
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) 
    {
        if(list1==nullptr)
        {
            return list2;
        }
        if(list2==nullptr)
        {
            return list1;
        }
        ListNode*newHead=nullptr;
        if(list1->val<list2->val)
        {
            newHead=mergeTwoLists(list1->next,list2);
            list1->next=newHead;
            return list1;
        }
        else
        {
            newHead=mergeTwoLists(list1,list2->next);
            list2->next=newHead;
            return list2;
        }
    }
};
(6)链表分割

链表分割_牛客题霸_牛客网 (nowcoder.com)

思路:管理两个链表

//(时间复杂度O(n),空间复杂度O(1))
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) 
    {
        ListNode*newHead1=new ListNode(0);
        newHead1->next=pHead;
        ListNode*newHead2=new ListNode(0);
        ListNode*cur=pHead;
        ListNode*newTail=newHead2;
        ListNode*pre=newHead1;
        while(cur)
        {
            if(cur->val<x)
            {
                newTail->next=cur;
                newTail=cur;
                pre->next=cur->next;
                cur=cur->next;
            }
            else 
            {
                pre=cur;
                cur=cur->next;
            }
        }
        newTail->next=newHead1->next;
        return newHead2->next;
    }
};
(7)链表的回文结构

链表的回文结构_牛客题霸_牛客网 (nowcoder.com)

思路1:递归

函数的功能:判断返回值真假和两边数值是否相等

结束条件:链表为空或链表只剩一个节点

等价关系:两边数值是否相等+回文结构

//(时间复杂度O(n),空间复杂度O(1))
class PalindromeList {
  public:
    ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
        if (pListHead == nullptr)
            return nullptr;
        ListNode* cur = pListHead;
        int n = 0;
        while (cur) {
            cur = cur->next;
            n++;
        }
        if (k > n)
            return nullptr;
        ListNode* slow = pListHead;
        ListNode* fast = pListHead;
        while (k--) {
            fast = fast->next;
        }
        while (fast) {
            fast = fast->next;
            slow = slow->next;
        }
        return slow;
    }
    bool chkPalindrome(ListNode* A) 
    {
        // write code here
        if(!A || !A->next)
        return true;
        //寻找倒数第二个结点
        ListNode*end=FindKthToTail(A, 2);
        if(A->val!=end->next->val)
        return false;
        end->next=nullptr;
        if(chkPalindrome(A->next))
        {
            return true;
        }
        return false;
    }
};

思路2:中间往后逆置,遍历比较

class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
        ListNode* dummy = (ListNode*)malloc(sizeof(ListNode));//哨兵结点
        dummy->next = A;
        ListNode*fast = dummy;
        ListNode*slow = dummy;
        while(fast && fast->next)  //获取中间结点
        {
            fast = fast->next->next;
            slow = slow->next;
        }
        
        ListNode* cur = slow->next; //反转链表
        ListNode* next = cur->next;
        ListNode* pre = slow;
        slow->next = NULL; //防止形成环!!!!!
        while(1)
        {
            cur->next = pre;
            pre = cur;
            if(next == NULL) //注意判断的是next而不是cur->next
                break;
            cur = next;
            next = cur->next;
        }
        
        ListNode* head = A;
        ListNode* tail = cur;
        while(tail != slow)
        {
            if(head->val != tail->val)
                return false;
            tail = tail->next;
            head = head->next;
        }
        return true;
    }
};
(8)相交链表

160. 相交链表 - 力扣(LeetCode)

思路:双指针

让两个链表的指针分别遍历自己的链表,当遍历到末尾时,让它从另一条链表的头开始再遍历

//(时间复杂度O(n+m),空间复杂度O(1))
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) 
    {
        if (!headA || !headB) return nullptr;  // 检查链表是否为空

        ListNode* cur1 = headA;
        ListNode* cur2 = headB;

        while (cur1 != cur2) 
        {
            cur1 = cur1 ? cur1->next : headB;  // 先移动指针,然后检查是否为nullptr
            cur2 = cur2 ? cur2->next : headA;
        }

        return cur1;  // 如果有交点,返回交点,否则返回nullptr
    }
};
(9)环形链表

141. 环形链表 - 力扣(LeetCode)

思路:快慢指针

//(时间复杂度O(n),空间复杂度O(1))
class Solution {
public:
    bool hasCycle(ListNode *head) 
    {
        ListNode*fast=head;
        ListNode*slow=head;
        while(fast && fast->next)
        {
            fast=fast->next->next;
            slow=slow->next;
            if(fast==slow)
            {
                return true;
            }
        }
        return false;
    }
};
(10)环形链表

142. 环形链表 II - 力扣(LeetCode)

思路:快慢指针

让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行, 两个指针都是每次均走一步,最终肯定会在入口点的位置相遇。

//(时间复杂度O(n),空间复杂度O(1))
class Solution {
public:
    ListNode *detectCycle(ListNode *head) 
    {
        ListNode*fast=head;
        ListNode*slow=head;
        while(fast && fast->next)
        {
            slow=slow->next;
            fast=fast->next->next;
            if(slow==fast)
            {
                slow=head;
                while(true)
                {
                    if(slow==fast)
                    {
                        return slow;
                    }
                    slow=slow->next;
                    fast=fast->next;
                }
            }
        }
        return nullptr;
    }
};
(11)复制带随机指针的链表

138. 复制带随机指针的链表 - 力扣(LeetCode)

思路:迭代 + 节点拆分

//(时间复杂度O(n),空间复杂度O(n))
class Solution {
public:
    Node* copyRandomList(Node* head) 
    {
        if (!head) return nullptr;

        // Step 1: Create a copy of each node and insert it right after the original node
        Node* cur = head;
        while (cur) {
            Node* newNode = new Node(cur->val);
            newNode->next = cur->next;
            cur->next = newNode;
            cur = newNode->next;
        }

        // Step 2: Update the random pointers of new nodes
        cur = head;
        while (cur) {
            if (cur->random) {
                cur->next->random = cur->random->next;
            }
            cur = cur->next->next;
        }

        // Step 3: Split the list to separate the original list and the copied list
        cur = head;
        Node* newHead = head->next;
        Node* newCur = newHead;
        while (cur) {
            cur->next = cur->next->next;
            if (newCur->next) {
                newCur->next = newCur->next->next;
            }
            cur = cur->next;
            newCur = newCur->next;
        }

        return newHead;
    }
};

3.2.5 优缺点

(1)无头单向非循环链表

适合头插、头删,不适合中间或尾部插入和删除

(2)带头双向循环链表

适合任意位置插入、删除,但不能随机访问

3.4 顺序表和链表对比

不同点顺序表链表
存储空间上物理上连续物理上不连续
随机访问支持不支持
任意位置插入或删除效率低效率高
应用场景频繁访问频繁插入、删除
缓存利用率
内存碎片较多较少

3.4.1 CPU高速缓存命中率

(1)顺序表

顺序表通常存储在连续的内存地址中,因此它们更利于CPU高速缓存的预取策略。当访问一个数组元素时,高速缓存可能会加载该元素所在的整个缓存行,从而也加载了其它临近的数组元素。因此,顺序遍历数组时,后续的元素很可能已经被预取到高速缓存中,从而提高了高速缓存命中率。

(2)链表

链表的节点分散在内存中,可能不是连续的。当你访问一个链表节点时,其它的节点并不一定被预取到高速缓存中,导致高速缓存命中率降低。每次访问链表的新节点时,都可能需要从主存中加载该节点,导致高速缓存未命中。

3.4.2 内存碎片

(1)顺序表

顺序表通常使用连续的内存块来存储数据。当顺序表需要增长时(例如,在向std::vector添加元素时),可能需要重新分配一个更大的内存块并将旧数据复制到新位置,然后释放旧的内存块。如果系统的可用内存是分散的,这可能导致外部碎片化。同时,如果顺序表预分配的空间(例如std::vector的容量)远大于实际使用的空间,那么可能会导致内部碎片化。

(2)链表

链表使用非连续的内存块(称为节点)来存储数据。每个节点都有一个数据元素和一个或多个指向其他节点的指针。因此,链表通常不受外部碎片化的影响,因为它们可以利用任何大小和位置的空闲内存块。但是,每个节点的额外指针存储开销可能导致内部碎片化,尤其是在存储小数据元素的情况下。

四、栈、队列

4.1 栈

一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。

进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。

栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。

压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。

出栈:栈的删除操作叫做出栈。出数据也在栈顶

4.1.1 结构

栈的实现一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小。

// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
 STDataType* _a;//动态数组
 int _top; // 栈顶
 int _capacity; // 容量
}Stack;

4.1.2 接口实现

具体实现参考gitee仓库:https://gitee.com/JIzaodeyy/data-structure.git

// 初始化栈
void StackInit(Stack* ps); 
// 入栈
void StackPush(Stack* ps, STDataType data); 
// 出栈
void StackPop(Stack* ps); 
// 获取栈顶元素
STDataType StackTop(Stack* ps); 
// 获取栈中有效元素个数
int StackSize(Stack* ps); 
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps); 
// 销毁栈
void StackDestroy(Stack* ps); 

4.1.3 相关笔试题

(1)有效括号匹配

20. 有效的括号 - 力扣(LeetCode)

思路:哈希映射+辅助栈

//(时间复杂度O(n),空间复杂度O(n))
class Solution {
public:
    bool isValid(string s) 
    {
        int n = s.size();
        if (n % 2 == 1) 
        {
            return false;
        }
        unordered_map<char, char> pairs = {
            {')', '('},
            {']', '['},
            {'}', '{'}
        };
        stack<char> stk;
        for (char ch: s) 
        {
            if (pairs.count(ch)) 
            {
                if (stk.empty() || stk.top() != pairs[ch]) 
                {
                    return false;
                }
                stk.pop();
            }
            else 
            {
                stk.push(ch);
            }
        }
        return stk.empty();
    }
};
(2)用栈实现队列

232. 用栈实现队列 - 力扣(LeetCode)

class MyQueue {
    public:
    MyQueue() {}
    void push(int x) 
    {
        stack1.push(x);
    }

    int pop() 
    {
        if (stack2.empty()) {
            while (!stack1.empty()) {
                stack2.push(stack1.top());
                stack1.pop();
            }
        }
        int front = stack2.top();
        stack2.pop();
        return front;
    }

    int peek() 
    {
        if (stack2.empty()) {
            while (!stack1.empty()) {
                stack2.push(stack1.top());
                stack1.pop();
            }
        }
        return stack2.top();
    }

    bool empty() 
    {
        return stack2.empty()&&stack1.empty();
    }
    private:
    stack<int> stack1;
    stack<int> stack2;
};

4.1.4 应用场景

  • 回溯算法,如深度优先搜索。
  • 支持撤销操作。
  • 解析表达式,如括号匹配。
  • 跟踪函数调用(计算机的调用堆栈)。

4.2 队列

只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表。

队列具有先进先出 FIFO(First In First Out)

入队列:进行插入操作的一端称为队尾

出队列:进行删除操作的一端称为队头

4.2.1 结构

队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。

// 链式结构:表示队列
typedef struct QListNode
{ 
    struct QListNode* _pNext; 
    QDataType _data; 
}QNode; 
// 队列的结构
typedef struct Queue
{ 
    QNode* _front; 
    QNode* _rear; 
}Queue; 

4.2.2 接口实现

具体实现参考gitee仓库:https://gitee.com/JIzaodeyy/data-structure.git

// 初始化队列
void QueueInit(Queue* q); 
// 队尾入队列
void QueuePush(Queue* q, QDataType data); 
// 队头出队列
void QueuePop(Queue* q); 
// 获取队列头部元素
QDataType QueueFront(Queue* q); 
// 获取队列队尾元素
QDataType QueueBack(Queue* q); 
// 获取队列中有效元素个数
int QueueSize(Queue* q); 
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q); 
// 销毁队列
void QueueDestroy(Queue* q);

4.2.3 循环队列

循环队列(Circular Queue)是一种数据结构,它使用一个固定大小的数组并维护两个指针:一个指向队列的开始(front),另一个指向队列的结束(rear)。循环队列的主要优点是当队列满时,我们可以从数组的开始处再次开始插入元素,前提是该位置是空的。这样,我们可以最大限度地利用数组的空间,避免在非循环队列中由于数组的开始部分没有元素而浪费空间的情况。

如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。

4.2.4 相关笔试题

(1)用队列实现栈

225. 用队列实现栈 - 力扣(LeetCode)

思路:

核心思想是在每次push操作时,将新元素放入空队列中,然后将另一个队列的所有元素依次移入新元素所在的队列,这样新元素就位于队列的前端,实现了栈的"后入先出"特性。

class MyStack {
public:
    MyStack() 
    {}
    
    void push(int x) 
    {
        if(queue1.empty())
        {
            queue1.push(x);
            while(!queue2.empty())
            {
                queue1.push(queue2.front());
                queue2.pop();
            }
        }
        else
        {
            queue2.push(x);
            while(!queue1.empty())
            {
                queue2.push(queue1.front());
                queue1.pop();
            }
        }
    }
    
    int pop() 
    {
        int ret;
        if(!queue1.empty()) 
        {
            ret = queue1.front();
            queue1.pop();
            return ret;
        }
        if(!queue2.empty()) 
        {
            ret = queue2.front();
            queue2.pop();
            return ret;
        }
        throw runtime_error("Stack is empty!");
    }
    
    int top() 
    {
        if(!queue1.empty())
        return queue1.front();
        if(!queue2.empty())
        return queue2.front();
        throw runtime_error("Stack is empty!");
    }
    
    bool empty() 
    {
        return queue1.empty()&&queue2.empty();
    }
private:
    queue<int> queue1;
    queue<int> queue2;
};

(2)设计循环队列

622. 设计循环队列 - 力扣(LeetCode)

思路:

enQueue()

  • 检查队列是否已满。
  • rear位置插入元素。
  • 更新rear指针。

deQueue()

  • 检查队列是否为空。
  • 返回front位置的元素。
  • 更新front指针。
//采用了[]的实现方式
class MyCircularQueue {
public:
    MyCircularQueue(int k) 
    {
        data.resize(k);
        front=rear=-1;
        size=0;
        capacity=k;
    }
    
    bool enQueue(int value) 
    {
        if (isFull()) return false;    
        if (isEmpty()) front = 0;     
        rear = (rear + 1) % capacity;
        data[rear] = value;
        size++;
        
        return true;
    }
    
    bool deQueue() 
    {
        if (isEmpty()) return false;
        
        if (front==rear) 
        {
            front = -1;
            rear = -1;
        } 
        else 
        {
            front = (front + 1) % capacity;
        }
        size--;
        
        return true;
    }
    
    int Front() 
    {
        if(!isEmpty())
        {
            return data[front];
        }
        return -1;
    }
    
    int Rear() 
    {
        if(!isEmpty())
        {
            return data[rear];
        }
        return -1;

    }
    
    bool isEmpty() 
    {
        return size==0;
    }
    
    bool isFull() 
    {
        return size==capacity;
    }
private:
    vector<int> data;
    int front;
    int rear;
    int size;
    int capacity;
};

4.2.5 应用场景

  • 广度优先搜索。
  • 缓存策略,如先进先出 (FIFO) 替换策略。
  • 调度算法,如轮询和优先级调度。

五、二叉树

5.1 树

树是一个由节点组成的集合。这个集合可以为空;若不为空,则它由一个称为根(Root)的特殊节点以及零个或多个子树组成,每个子树也是一个树。

树是递归定义

5.1.1 树的相关概念

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6

叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点

非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G…等节点为分支节点

双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点

孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点

兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点

树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6

节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;

树的高度或深度:树中节点的最大层次; 如上图:树的高度为4

堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点

节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先

子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙

森林:由m(m>0)棵互不相交的树的集合称为森林;

5.1.2 树的存储结构

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。

typedef int DataType;
struct Node
{
 struct Node* _firstChild1; // 第一个孩子结点
 struct Node* _pNextBrother; // 指向其下一个兄弟结点
 DataType _data; // 结点中的数据域
};

5.1.3 树的应用

  1. 文件系统: 许多操作系统的文件系统都是以树结构组织的,其中目录可以被视为内部节点,文件可以被视为叶节点。
  2. 组织结构: 许多组织使用树形结构来表示员工和管理层之间的关系。
  3. HTML DOM: 网页的结构是由文档对象模型(DOM)表示的,这是一个树形结构,其中每个元素、属性和文本都是一个节点。
  4. 解析表达式: 编译器和解释器常使用语法分析树(AST)来解析并表示源代码中的结构。
  5. 路由协议: 许多网络路由协议使用树来计算和表示数据包的最佳路径。
  6. 数据库索引: 树结构如B树和B+树经常用于数据库系统中,用于高效地查找和存储数据。
  7. 搜索算法: 在AI和游戏程序中,搜索算法如MinMax和Alpha-Beta剪枝使用树来表示所有可能的游戏状态。
  8. 压缩算法: Huffman编码使用树来压缩数据。
  9. 图形学: 在计算机图形学中,场景图(Scene Graph)是一个树形结构,用于表示3D场景中的对象。
  10. 决策树: 在机器学习中,决策树是一种用于分类和回归的方法。
  11. Trie(前缀树): 用于实现字典、搜索建议和其他与文本相关的数据结构。

5.2 二叉树

二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”和“右子树”。从形式上定义,二叉树或者是空的(空树),或者是由一个称为根的元素及两个互不相交的、分别称为左子树和右子树的二叉树组成。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

5.2.1 二叉树的性质

  1. 节点的度数:在二叉树中,每个节点的子节点数不超过2。
  2. 关于节点的层数:在二叉树的第i层上至多有 (2i-1) 个结点(i≥1)。
  3. 深度为k的二叉树:深度为k的二叉树至多有 (2k-1) 个节点(k≥1)。
  4. 节点数量与边的关系:对于任何非空二叉树,如果其叶节点数为N0,度为2的节点数为N2,则N0 = N2 + 1。这个性质是基于每新增一个度为2的节点就意味着增加了两个新的叶子节点,但是原来的叶子节点中一个已经变成了度为2的节点,因此总的叶子节点数量只增加1。

5.2.2 特殊二叉树

  1. 满二叉树:一个深度为k且含有 2k -1个节点的二叉树被称为满二叉树。
  2. 完全二叉树:深度为k的二叉树,当且仅当其每一个层的节点数都达到最大值(除最后一层外,它的节点都靠左连续排列)时,它就是完全二叉树。
  3. 平衡二叉树(AVL树):对于树中的每个节点,其左子树和右子树的深度之差的绝对值不超过1。
  4. 二叉搜索树:对于树中的每个节点,其左子树的所有节点的值都小于这个节点的值,而其右子树的所有节点的值都大于这个节点的值。

5.2.3 二叉树的存储结构

(1)顺序存储
  • 二叉树的每个节点都有一个唯一的下标,并按照层次顺序存储在数组中。
  • 对于任意位置为i的节点(下标从1开始计算):
    • 如果它存在左子节点,那么左子节点的位置为2i。
    • 如果它存在右子节点,那么右子节点的位置为2i+1。
    • 它的父节点位置为i/2(整数除法)。
  • 顺序存储结构更适用于完全二叉树,因为对于非完全二叉树可能会造成存储空间的浪费。
(2)链式存储

二叉树的每个节点由一个结构或类定义,通常包含三个字段:一个用于存储节点数据的字段,一个指向左子节点的指针(或引用),一个指向右子节点的指针。

链式结构又分为二叉链和三叉链,后续如红黑树等会用到三叉链

struct TreeNode {
    int value;
    TreeNode* left;
    TreeNode* right;
};

5.3 堆

堆是一种特殊的完全二叉树

5.3.1 堆的存储结构

堆通常使用数组来存储其结构,尽管它在逻辑上被表示为完全二叉树。这是因为完全二叉树的特性非常适合使用数组进行存储,而不需要指针或其他数据结构。使用数组来存储堆可以有效地减少空间和时间开销。

(1)索引关系

对于数组中的任意位置i,我们可以快速找到其父节点、左孩子和右孩子:

  • 父节点:位置为 (i - 1) / 2 (这里的除法是整数除法)
  • 左孩子:位置为 2i + 1
  • 右孩子:位置为 2i + 2
(2)插入元素

当要插入一个新元素时,我们首先将其放在数组的最后一个位置,然后执行上浮操作来保证堆的性质。

(3)删除元素

通常,在堆中执行的删除操作是删除最大元素(在最大堆中)或最小元素(在最小堆中)。为此,我们首先将最后一个元素移动到根的位置,然后执行下沉操作来恢复堆的性质。

5.3.1 最大堆

任何一个父节点的值都大于或等于它的孩子节点的值。

5.3.2 最小堆

任何一个父节点的值都小于或等于它的孩子节点的值。

5.3.3 堆的实现

(1)向下调整算法

向下调整算法,也被称为“堆化”或“下沉”算法,是用于维护堆属性的一种方法。当堆的某个节点的值不满足堆的性质时(例如,在最大堆中,该节点的值小于其孩子的值),我们需要对其进行向下调整,以重新满足堆的性质。

//最大堆的向下调整算法
//从当前节点开始,将其与其左右孩子中的最大值进行比较,如果当前节点小于其孩子,则与其孩子中的最大值交换,并递归地继续向下调整。
//时间复杂度是O(log n)
//数组区间[),即n表示节点个数
void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}
void MaxHeapify(int arr[], int i, int n) {
    int parent = i;
    int child=2*i+1;// 左孩子的索引

    // 更新最大孩子
    if (child + 1 < n && arr[child] < arr[child+1]) {
        child=child+1;
    }
    //如果最大孩子比父节点值大就交换
    if(arr[child] > arr[parent])
    {
        swap(&arr[child],&arr[parent]);
        // 递归地向下调整
        MaxHeapify(arr, child, n);
    }
}

(2)堆化

将一个数组创建为大顶堆的过程称为堆化。

具体来说,这是一个自底向上的过程。我们从最后一个非叶子节点开始,依次对每个节点执行向下调整算法,直到根节点。经过这个过程,数组就变成了一个大顶堆。

尽管向下调整的最坏情况是O(log n),但在构建过程中,大部分的节点都非常接近底部,因此他们的子节点非常少。这意味着大部分的节点只需要非常少的操作就可以完成向下调整。而少数需要进行更多操作的节点(即接近根的节点)数量又非常少。

这种结构确保了整体的工作量保持在O(n)。

//建立大堆
//时间复杂度是O(n)
void BuildMaxHeap(int arr[], int n) {
    // 从最后一个非叶子节点开始(索引为 (n/2 - 1))
    for (int i = n/2 - 1; i >= 0; i--) {
        MaxHeapify(arr, i, n);
    }
}
(3)向上调整算法

向上调整是另一种常见的调整方法,用于将元素插入到堆中。当在堆的末尾添加一个新元素后,可能会违反堆的性质。为了修复这一点,我们可以使用向上调整。

向上调整的思路是将新插入的节点与其父节点进行比较:

  • 对于大顶堆:如果新节点的值大于其父节点的值,我们交换两者的位置。
  • 对于小顶堆:如果新节点的值小于其父节点的值,我们交换两者的位置。
//大堆向上调整算法
//时间复杂度是O(logn)
void UpwardAdjustment(int arr[], int child) 
{
    int parent = (child - 1) / 2; // 计算父节点索引
    if(child > 0 && arr[child] > arr[parent]) 
    {
        // 交换父节点和子节点的值
        swap(&arr[child], &arr[parent]);
        // 递归地向上调整
        UpwardAdjustment[arr,parent]
    }
}
(4)插入
  1. 将新元素添加到数组的末尾。
  2. 执行向上调整算法。
void insert(int arr[], int val, int &n, int capacity) {
    if (n >= capacity) {
        throw runtime_error("Array is full");
    }

    arr[n] = val;  // 插入到数组的末尾
    UpwardAdjustment(arr, n);  // 对最后一个元素执行向上调整
    n++;  // 增加当前元素数
}
(5)删除
  1. 用数组的最后一个元素替换堆顶元素。
  2. 删除数组的最后一个元素。
  3. 从根开始执行向下调整。
int remove(int arr[], int &n) {
    if (n == 0) {
        throw runtime_error("Array is empty");
    }
    int top = arr[0];
    arr[0] = arr[n-1];
    n--;  // 减少当前元素数
    DownwardAdjustment(arr, 0, n);  // 对根元素执行向下调整
    return top;
}

5.3.4 堆的应用

(1)堆排序

堆排序是一个使用堆的比较排序算法

  1. 建堆:将输入数据建立成一个大顶堆(或小顶堆,如果需要升序排序)。
  2. 堆调整和排序:删除堆的顶部元素(最大或最小),然后重新调整堆。重复此过程,直到堆为空。
void HeapSort(int arr[], int n) {
    // 1. 建堆
  	BuildMaxHeap(int arr[], int n)

    // 2. 堆调整和排序
    for (int i = n - 1; i > 0; i--) {
        // 交换堆顶元素和末尾元素,将最大元素放到最后,形成升序
        swap(arr[0], arr[i]);
        // 重新调整堆
        DownwardAdjustment(arr, 0, i - 1);
    }
}

(2)TOP-K问题

TOK-K问题,也被称为找到前K个最大或最小的元素,是一个经常在面试和算法比赛中遇到的问题。使用堆是解决这个问题的一个非常高效的方法。具体来说,对于找到前K个最小的元素,我们可以使用一个大小为K的大顶堆;对于找到前K个最大的元素,我们可以使用一个大小为K的小顶堆。

  1. 首先,将数组的前K个元素插入一个大顶堆中。
  2. 从数组的第K + 1个元素开始遍历到最后一个元素:
    • 对于数组中的每一个元素,如果它小于堆顶的元素(也就是这个大顶堆中的最大元素),那么就删除堆顶的元素并将这个元素插入堆中。
  3. 当数组中所有元素都被遍历过后,堆中的K个元素就是数组中的前K个最小的元素。
void swap(int* a, int* b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
// 向下调整(适用于小根堆)
void DownwardAdjustment(int arr[], int i, int n) {
    int parent = i;
    int child = 2 * parent + 1; // 左孩子
    while (child < n) {
        if (child + 1 < n && arr[child] > arr[child + 1]) { 
            child++; // 如果右孩子存在且小于左孩子,child指向右孩子
        }
        if (arr[parent] <= arr[child]) {
            break;
        }
        swap(&arr[parent], &arr[child]);
        parent = child;
        child = 2 * parent + 1;
    }
}

// 创建小根堆
void BuildMinHeap(int arr[], int k) {
    for (int i = k / 2 - 1; i >= 0; i--) {
        DownwardAdjustment(arr, i, k);
    }
}

void TopK(int arr[], int n, int k) {
    if (k <= 0 || k > n) return;

    // 初始化大小为K的堆
    int heap[k];
    for (int i = 0; i < k; i++) {
        heap[i] = arr[i];
    }
    BuildMinHeap(heap, k);

    // 遍历其余元素,与堆顶比较
    for (int i = k; i < n; i++) {
        if (arr[i] > heap[0]) {
            heap[0] = arr[i];
            DownwardAdjustment(heap, 0, k);
        }
    }

    // 输出TOP-K
    printf("Top %d elements are: ", k);
    for (int i = 0; i < k; i++) {
        printf("%d ", heap[i]);
    }
    printf("\n");
}

5.4 二叉树链式结构

5.4.1 遍历

(1)前、中、后序遍历
// 前序遍历
void PreorderTraversal(TreeNode* root) {
    if (root == NULL) return;
    printf("%d ", root->val);           // 访问根节点
    PreorderTraversal(root->left);      // 前序遍历左子树
    PreorderTraversal(root->right);     // 前序遍历右子树
}

// 中序遍历
void InorderTraversal(TreeNode* root) {
    if (root == NULL) return;
    InorderTraversal(root->left);       // 中序遍历左子树
    printf("%d ", root->val);           // 访问根节点
    InorderTraversal(root->right);      // 中序遍历右子树
}

// 后序遍历
void PostorderTraversal(TreeNode* root) {
    if (root == NULL) return;
    PostorderTraversal(root->left);     // 后序遍历左子树
    PostorderTraversal(root->right);    // 后序遍历右子树
    printf("%d ", root->val);           // 访问根节点
}
(2)层序遍历

层次遍历,也称为广度优先遍历,是一种从根节点开始,按照层次从上到下、从左到右的顺序遍历整个二叉树的方法。为了实现层次遍历,我们通常使用一个队列来辅助。

void levelOrderTraversal(TreeNode* root) {
    if (!root) return;

    queue<TreeNode*> q;
    q.push(root);

    while (!q.empty()) 
    {
        //取出队头
        TreeNode* currentNode = q.front();
        q.pop();
        cout << currentNode->val << " ";
		//将对头的左右孩子放进队列
        if (currentNode->left) {
            q.push(currentNode->left);
        }
        if (currentNode->right) {
            q.push(currentNode->right);
        }
    }
}

5.4.2 部分接口

(1)节点个数以及高度
// 二叉树节点个数
int BinaryTreeSize(BTNode* root) {
    if (!root) return 0;
    //前序遍历
    return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root) {
    if (!root) return 0;
    //前序遍历
    if (!root->left && !root->right) return 1;
    return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k) {
    if (!root ) return 0;
    //前序遍历
    if (k == 1) return 1;
    //将以root为起点时,求k深度的节点个数,转化为以他们孩子为起点时,求k-1深度的节点个数的和
    return BinaryTreeLevelKSize(root->left, k-1) + BinaryTreeLevelKSize(root->right, k-1);
}

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x) {
    if (!root) return NULL;
    //前序遍历
    if (root->data == x) return root;
    BTNode* leftFind = BinaryTreeFind(root->left, x);
    if (leftFind) return leftFind;
    return BinaryTreeFind(root->right, x);
}

(2) 二叉树的创建和销毁
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi) {
    if (a[*pi] == '#' || *pi >= strlen(a)) {
        (*pi)++;
        return NULL;
    }
    BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));
    newNode->data = a[*pi];
    (*pi)++;
    newNode->left = BinaryTreeCreate(a, pi);
    newNode->right = BinaryTreeCreate(a, pi);
    return newNode;
}
// 二叉树销毁
void BinaryTreeDestroy(BTNode** root) {
    if (*root) {
        //后序遍历
        BinaryTreeDestroy(&((*root)->left));
        BinaryTreeDestroy(&((*root)->right));
        free(*root);
        *root = NULL;
    }
}
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root) {
    if (!root) return 1;

    std::queue<BTNode*> q;
    q.push(root);
     // 判断是否进入后续阶段,即当出现右孩子为空时,该右孩子右侧不应该再出现节点
    int flag = 0;

    while (!q.empty()) 
    {
        BTNode* curr = q.front();
        q.pop();
        if (!flag) 
        {
            if (curr->left && curr->right) 
            {
                q.push(curr->left);
                q.push(curr->right);
            } 
            else if (!curr->left && curr->right) 
            {
                // 左子树为空,右子树不为空
                return false;  
            } 
            else if (curr->left && !curr->right) 
            {
                // 左子树不为空,右子树为空
                q.push(curr->left);
                // 进入后续阶段
                flag = 1; 
            } 
            else 
            {
                // 进入后续阶段
                flag = 1; 
            }
        } 
        else 
        {
            if (curr->left || curr->right) 
            {
                // 进入后续阶段,但仍有子节点
                return false;  
            }
        }
    }
    return true; // 完全二叉树
}

5.5 相关笔试题

(1)单值二叉树

965. 单值二叉树 - 力扣(LeetCode)

思路:前序遍历,深度优先算法

class Solution {
public:
    bool isUnivalTree(TreeNode* root) 
    {
        if(root==nullptr) return true;
        if(root->right&&root->val!=root->right->val) return false;
        if(root->left&&root->val!=root->left->val) return false; 
        return isUnivalTree(root->right) && isUnivalTree(root->left);
    }
};

(2)相同的树

100. 相同的树 - 力扣(LeetCode)

class Solution {
public:
    bool isSameTree(TreeNode* p, TreeNode* q) 
    {
        if(p==nullptr&&q==nullptr) return true;
        //
        if(p==nullptr) return false;
        if(q==nullptr) return false;
        if(p->val!=q->val) return false;
        return isSameTree(p->right,q->right) && isSameTree(p->left,q->left);
    }
};

(3)对称二叉树

101. 对称二叉树 - 力扣(LeetCode)

思路:前序遍历,深度优先搜索

class Solution {
public:
    bool _isSymmetric(TreeNode* root1,TreeNode*root2)
    {
        if(root1==nullptr&&root2==nullptr) return true;
        if(root1==nullptr) return false;
        if(root2==nullptr) return false;
        if(root1->val!=root2->val) return false;
        return _isSymmetric(root1->left,root2->right) && _isSymmetric(root1->right,root2->left);
    } 
    bool isSymmetric(TreeNode* root) 
    {
        if(root==nullptr) return true;
        return _isSymmetric(root->left,root->right);
    }
};

(4)二叉树的前序遍历

144. 二叉树的前序遍历 - 力扣(LeetCode)

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) 
    {
        if (root == nullptr) return _arr;
        _arr.push_back(root->val);           // 访问根节点
        preorderTraversal(root->left);      // 前序遍历左子树
        preorderTraversal(root->right);     // 前序遍历右子树
        return _arr;
    }
private:
    vector<int> _arr;
};

(5)二叉树的中序遍历

94. 二叉树的中序遍历 - 力扣(LeetCode)

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) 
    {
        if (root == nullptr) return _arr;
        inorderTraversal(root->left);      // 前序遍历左子树
         _arr.push_back(root->val);           // 访问根节点
        inorderTraversal(root->right);     // 前序遍历右子树
        return _arr;
    }
private:
    vector<int> _arr;
};

(6)二叉树的后序遍历

145. 二叉树的后序遍历 - 力扣(LeetCode)

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) 
    {
        if (root == nullptr) return _arr;
        postorderTraversal(root->left);      // 前序遍历左子树
        postorderTraversal(root->right);     // 前序遍历右子树
         _arr.push_back(root->val);           // 访问根节点
        return _arr;
    }
private:
    vector<int> _arr;
};

(7)另一课树的子树

572. 另一棵树的子树 - 力扣(LeetCode)

思路:

这段代码首先定义了一个check函数,用于检查两棵树是否完全相同。接着,isSubtree函数检查子树subRoot是否是树root的子结构。如果subRootroot的子结构或者subRootroot的左/右子树的子结构,则返回true,否则返回false

class Solution {
public:
    bool check(TreeNode *root, TreeNode *subRoot) {
        if (root == nullptr && subRoot == nullptr) return true;
        if ((root != nullptr && subRoot == nullptr) || (root == nullptr && subRoot != nullptr) || (root->val != subRoot->val)) return false;

        return check(root->left, subRoot->left) && check(root->right, subRoot->right);
    }

    bool isSubtree(TreeNode* root, TreeNode* subRoot) {
        if (root == nullptr) return false;
        return check(root, subRoot) || isSubtree(root->right, subRoot) || isSubtree(root->left, subRoot);
    }
};

(8)二叉树遍历

二叉树遍历_牛客题霸_牛客网 (nowcoder.com)

#include <iostream>
using namespace std;
struct BTnode
{
    char val;
    BTnode* left;
    BTnode* right;
};
BTnode*BinaryTreeCreate(string s,int *pi)
{   
    if(s[*pi]=='#'||*pi>=s.size()) 
    {
        (*pi)++;
        return nullptr;
    }
    BTnode*newNode=new BTnode();
    newNode->val=s[*pi];
    (*pi)++;
    newNode->left=BinaryTreeCreate(s,pi);
    newNode->right=BinaryTreeCreate(s,pi);
    return newNode;
}
void InorderTraversal(BTnode* root) {
    if (root == nullptr) return;
    InorderTraversal(root->left);       // 中序遍历左子树
    printf("%c ", root->val);           // 访问根节点
    InorderTraversal(root->right);      // 中序遍历右子树
}
void BinaryTreeDestroy(BTnode* root) {
    if (root == nullptr) return;
    BinaryTreeDestroy(root->left);
    BinaryTreeDestroy(root->right);
    delete root;
}
int main() 
{
    string s;
    while (cin >> s) 
    { // 注意 while 处理多个 case
        int i=0;
        //创建二叉树
        BTnode*root=BinaryTreeCreate(s,&i);
        //中序遍历
        InorderTraversal(root);
        //销毁
        BinaryTreeDestroy(root);
    }
}

树的子树

572. 另一棵树的子树 - 力扣(LeetCode)

思路:

这段代码首先定义了一个check函数,用于检查两棵树是否完全相同。接着,isSubtree函数检查子树subRoot是否是树root的子结构。如果subRootroot的子结构或者subRootroot的左/右子树的子结构,则返回true,否则返回false

class Solution {
public:
    bool check(TreeNode *root, TreeNode *subRoot) {
        if (root == nullptr && subRoot == nullptr) return true;
        if ((root != nullptr && subRoot == nullptr) || (root == nullptr && subRoot != nullptr) || (root->val != subRoot->val)) return false;

        return check(root->left, subRoot->left) && check(root->right, subRoot->right);
    }

    bool isSubtree(TreeNode* root, TreeNode* subRoot) {
        if (root == nullptr) return false;
        return check(root, subRoot) || isSubtree(root->right, subRoot) || isSubtree(root->left, subRoot);
    }
};

(8)二叉树遍历

二叉树遍历_牛客题霸_牛客网 (nowcoder.com)

#include <iostream>
using namespace std;
struct BTnode
{
    char val;
    BTnode* left;
    BTnode* right;
};
BTnode*BinaryTreeCreate(string s,int *pi)
{   
    if(s[*pi]=='#'||*pi>=s.size()) 
    {
        (*pi)++;
        return nullptr;
    }
    BTnode*newNode=new BTnode();
    newNode->val=s[*pi];
    (*pi)++;
    newNode->left=BinaryTreeCreate(s,pi);
    newNode->right=BinaryTreeCreate(s,pi);
    return newNode;
}
void InorderTraversal(BTnode* root) {
    if (root == nullptr) return;
    InorderTraversal(root->left);       // 中序遍历左子树
    printf("%c ", root->val);           // 访问根节点
    InorderTraversal(root->right);      // 中序遍历右子树
}
void BinaryTreeDestroy(BTnode* root) {
    if (root == nullptr) return;
    BinaryTreeDestroy(root->left);
    BinaryTreeDestroy(root->right);
    delete root;
}
int main() 
{
    string s;
    while (cin >> s) 
    { // 注意 while 处理多个 case
        int i=0;
        //创建二叉树
        BTnode*root=BinaryTreeCreate(s,&i);
        //中序遍历
        InorderTraversal(root);
        //销毁
        BinaryTreeDestroy(root);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值