数据结构与算法

目录

一、树

1.真二叉树

2.满二叉树

3.完全二叉树

4.二叉排序树(想一想二分法)

5.平衡二叉树

(1)平衡因子 BF(Balance Factor )

(2)2 种「旋转」方式

二、彻底理解二叉树的遍历 

1.回溯法

2.二叉树遍历方式

3.递归实现遍历二叉树

(1)二叉树结构

4.非递归的遍历树

三、彻底理解堆 

1.数组转换为堆

2.删除堆节点

3.增加节点

4.堆的应用

(1)数组排序

(2)求数组中最小的N个数

四、看完这篇还不懂链表你来打我

五、链表排序都写不出来能通过BAT面试吗? 

1.冒泡排序

2.选择排序

3.插入排序

4.快速排序

5.归并排序

六、优先级队列

1.下沉sink与上浮swim

七、数据结构是如何装入 CPU 寄存器的? 

1.内存读写指令

2.编译器


一、树

        树是n(n>=0)个节点的有限集。当n=0时,称为空树。m(m>=0)棵互不相交的树组成的集合,称为森林。

  • 结点:树中的一个独立单元。包含一个数据元素及若干指向其子树的分支,如上图中的A、B、C、D等。
  • 结点的度:结点拥有的子树数。例如,A的度为3,C的度为1,F的度为0。
  • 树的度:树的度是树内各结点度的最大值。上图中所示的树的度为3
  • 叶子:度为0的结点称为叶子或终端结点。结点K、L、F,G、M、1、J都是树的叶子。
  • 非终端结点:度不为0的结点称为非终端结点或分支结点。除根结点之外,非终端结点也称为内部结点。
  • 双亲和孩子:结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。例如,B的双亲为A,B的孩子有E和F。
  • 兄弟:同一个双亲的孩子之间互称兄弟。例如,H、1和J互为兄弟。
  • 堂兄弟:双亲在同一层的结点。例如,结点G与E,F,H.1.J互为堂兄弟。
  • 祖先:从根到该结点所经分支上的所有结点。例如,M的祖先为A、D和H.
  • 子孙:以某结点为根的子树中的任一结点都称为该结点的子孙。如B的子孙为E、K、L和F。
  • 层次:结点的层次从根开始定义起,根为第一层,根的孩子为第二层。树中任一结点的层次等于其双亲结点的层次加1。
  • 树的深度:树中结点的最大层次称为树的深度或高度。图中所示的树的深度为4
  • 有序树和无序树:如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。

1.真二叉树

        所有节点的度都要么为0,要么为2。

2.满二叉树

3.完全二叉树

4.二叉排序树(想一想二分法)

  • 左子树所有的关键字小于根结点的关键字
  • 右子树所有的关键字大于根节点的关键字
  • 左子树和右子树又各是一个二叉排序树

5.平衡二叉树

        任何一结点的左子树和右子树的深度之差不超过1.

        判断「平衡二叉树」的 2 个条件:

  • 1. 是「二叉排序树」
  • 2. 任何一个节点的左子树或者右子树都是「平衡二叉树」(左右高度差小于等于 1)

    

左边是平衡二叉树                    右边不是平衡二叉树

(1)平衡因子 BF(Balance Factor )

定义:左子树和右子树高度差

计算:左子树高度 - 右子树高度的值

一般来说 BF 的绝对值大于 1,平衡树二叉树就失衡,需要「旋转」纠正。

(2)2 种「旋转」方式

  1. 左旋
    1. 旧根节点为新根节点的左子树
    2. 新根节点的左子树(如果存在)为旧根节点的右子树
  2. 右旋:
    1. 旧根节点为新根节点的右子树
    2. 新根节点的右子树(如果存在)为旧根节点的左子树

二、彻底理解二叉树的遍历 

        在计算机科学中二叉树,binary tree,是一种数据结构,在该数据结构中每个节点最多有两个子节点。

1.回溯法

        回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

        比如二叉树中左中右遍历,从叶子节点返回到父节点就叫回溯。

2.二叉树遍历方式

        先序遍历:根左右

        中序遍历:左根右

        后续遍历:左右根

3.递归实现遍历二叉树

(1)二叉树结构

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

前序遍历

void search_tree(struct tree* t) {
  if (t == NULL) // 如果是一颗空树则直接返回
    return;
     
  printf("%d ", t->value); // 打印根节点的值
  seartch_tree(t->left); // 打印树t的左子树
  search_tree(t->right); // 打印树t的右子树
}

中序遍历

void search_in_order(struct tree* t) {
    if (t == NULL) // 如果是一颗空树则直接返回
      return;
      
    search_in_order(t->left); // 打印树t的左子树
    printf("%d ", t->value); // 打印根节点的值
    search_in_order(t->right); // 打印树t的右子树
}

后序遍历

void search_post_order(struct tree* t) {
    if (t == NULL) // 如果是一颗空树则直接返回
      return;
      
    search_in_order(t->left); // 打印树t的左子树
    search_in_order(t->right); // 打印树t的右子树
    printf("%d ", t->value); // 打印根节点的值
}

4.非递归的遍历树

        递归调用会耗费很多的栈空间,也就是内存,同时该过程较为耗时,因此其性能通常不及非递归版本。

先序遍历

void search_pre_order(tree* root) {
    if(root == NULL)
        return ;
    stack<tree*>s;
    
    // 不管三七二十一先把从根节点开始的所有左子树节点放入栈中
    while(root){
        printf("%d ", root->value); // 节点入栈前打印 
        s.push(root);
        root=root->left;
    }
    
    while(!s.empty()){
        // 查看栈顶元素,如果栈顶元素有右子树那么右子树入栈并重复过程1直到栈空为止
        tree* top = s.top();
        tree* t = top->right;
        s.pop();
   
        while(t){
            printf("%d ", t->value); // 节点入栈前打印 
            s.push(t);
            t = t->left;
        }
    }
    return r;
}

        后序遍历:需要记录上一次处理的节点,因为在先序和中序遍历过程中,只要左子树处理完毕实际上栈顶元素就可以出栈了。但是后续遍历情况不同,只有左子树和右子树都遍历完毕才可以处理当前节点。

如果我们知道了遍历过程中的前一个节点,那么我们就可以做如下判断了:

  1. 如果前一个节点是当前节点的右子树,那么说明右子树遍历完毕可以pop了。
  2. 如果前一个节点是当前节点的左子树而且当前节点右子树为空,那么说明可以pop了。
  3. 如果当前节点的左子树和右子树都为空,也就是叶子节点,那么说明可以pop了。

三、彻底理解堆 

        堆是一种树。在树的性质之外,堆要求节点按照大小(父节点比子节点大/父节点比子节点小)来排列。

        二叉堆通常是一个可以被看做一棵近似完全二叉树的数组对象。为什么说近似呢?因为除完全二叉树的性质外,二叉堆还要求堆内元素按照大小排列:

  • 大根堆:堆中的每一个节点的值都大于等于左右子树节点,这被称为大根堆。
  • 小根堆:堆中每个一节点的值都小于等于左右子树节点,那么这被称为小根堆。

        对于二叉堆:

  • 每一个左子树节点的下标是父节点的2倍
  • 每一个右子树节点的下标是父节点的2倍再加1

1.数组转换为堆

        我们要想将一个数组转换为堆(以大根堆为例),我们只需要从第一个非叶子节点开始调整即可第一个非叶子节点总是最后一个节点的父节点。因此第一个非叶子节点就是:

parent(heap_size) == heap_size / 2

        然后和叶子节点比较大小,如果小于叶子节点,进行交换。

2.删除堆节点

        将删除的这个节点与最后一个节点进行交换后删除,再进行比较排序。

3.增加节点

        将增加的节点放在最后一个节点上,然后与父节点比较,排序。

4.堆的应用

(1)数组排序

        对于大根堆来说:堆中的第一个元素就是所有元素的最大值

        堆排序的时间复杂度为O(nlogn)。

  1. 将大根堆中的第一个元素和最后一个元素交换
  2. 堆大小减一
  3. 在第一个元素上进行排序,维持大根堆的性质
  4. 回到第一步

(2)求数组中最小的N个数

        求最小的N个数,需要建立大根堆;求最大的N个数,需要建立小根堆。

  1. 先将数组中前N个数建立一个大根堆;
  2. 从第N+1个元素循环遍历数组,与根节点比较:
  • 如果元素<根节点值,交换,大根堆重新排序;
  • 如果元素>根节点值,不操作。

        最终大根堆中存在的值就是最小的N个数。

四、看完这篇还不懂链表你来打我

        数据结构是一种组织数据的方式,和语言无关

  1. 数组是连续的、静态的,创建好后就不能改动;利于查找。
  2. 链表是可分散的、动态的,你可以根据需求来动态的增加或者减少链表的长度,利于增删。
struct node {
  struct node* next; // 下一节节点
  int value;         // 当前节点存的值
};

五、链表排序都写不出来能通过BAT面试吗? 

1.冒泡排序

        相邻两个比大小;

        时间复杂度O(n^2),空间复杂度O(1);

2.选择排序

        每次遍历选择最小(大)的;

        时间复杂度O(n^2),空间复杂度O(1);

3.插入排序

        现将第 n 个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。

        时间复杂度O(n^2),空间复杂度O(1);

4.快速排序

        有两个索引,i,j,array[i]与array[j]进行交换。其实每次比较的两个数中,其中一个就是选中的key。

  1. 找到数组中任意一个元素key(可能有的资料有不同的选取策略),刚开始array[i]就是key。
  2. 然后根据该元素将整个数组划分为两段,小于该元素的一半以及大于该元素的一半,然后将小于该元素的一半放到左边,把大于该元素的一半放到右边,把该元素放到中间,这样该元素就放到了最终位置上
  3. 对左边一半和右边一半重复上述过程

        时间复杂度O(nlog2n),空间复杂度O(nlog2n);

5.归并排序

        归并排序(Merge sort)是建立在归并操作上的一种有效、稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

        时间复杂度O(nlog2n)。

六、优先级队列

        优先队列的本质是堆,但它具有队列的所有操作特性,与普通队列不同的地方就是出队的时候按照优先级顺序出队,这个优先级即最大堆或最小堆的规则(即大的为top优先出队或小的为top优先出队),在队列的基础上加了个堆排序。

        优先级队列这种数据结构有一个很有用的功能,你插入或者删除元素的时候,元素会自动排序,这底层的原理就是二叉堆的操作。

        堆排序只能保证根是最大(最小),整个堆并不是有序的。所以可以自己实现排序。

//最小堆实现
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(10,new Comparator<Integer>(){
        public int compare(Integer a, Integer b){
            return a-b; //if a>b 则交换,so这是递增序列
        }
    });

//最小堆实现
PriorityQueue<ListNode> pq = new PriorityQueue<>(
        lists.length, (a, b)->(a.val - b.val));
//最小堆实现
 auto MyCompare=[](ListNode* l1,ListNode* l2){return l1->val>l2->val;};
 priority_queue<ListNode*,vector<ListNode*>,decltype(MyCompare)> pq(MyCompare);

1.下沉sink与上浮swim

        对于最大堆,会破坏堆性质的有两种情况:

  1. 如果某个节点 A 比它的子节点(中的一个)小,那么 A 就不配做父节点,应该下去,下面那个更大的节点上来做父节点,这就是对 A 进行下沉
  2. 如果某个节点 A 比它的父节点大,那么 A 不应该做子节点,应该把父节点换下来,自己去做父节点,这就是对 A 的上浮

        优先级队列是基于二叉堆实现的,主要操作是插入和删除。插入是先插到最后,然后上浮到正确位置;删除是把第一个元素 pq[1](最值)调换到最后再删除,然后把新的 pq[1] 下沉到正确位置。

七、数据结构是如何装入 CPU 寄存器的? 

        实际上我们没有必要一次性把整个数据全部装到CPU寄存器中,而是用到哪些才装载哪些。

1.内存读写指令

        寄存器是有限的,那么我们使用的庞大的数据结构是怎样装入寄存器供CPU计算的呢?

        机器指令中除了负责逻辑运算、执行流控制、函数调用等指令外,还有一类指令,这类执行只负责和内存打交道,典型的就是精简指令集架构中的Load/Store机器指令,即内存读写指令(复杂指令集没有单独的内存读写指令)。

        比如一个数组,可能会非常庞大(比如4G),但是具体到代码,每一个步骤操作的数据又会非常简单(我们一次只能操作一个元素),这一微小的操作使用的基本元素都可以通过内存读写指令加载到寄存器,修改完后再写回内存。

2.编译器

        内存中的数据要远大于CPU寄存器的容量,因此编译器必须精心挑选,好让那些经常使用的数据(比如数组的首地址)放到寄存器中的时间更长一点,这样可以减少内存读写次数。

        编译器把那些经常使用的数据放到寄存器,剩下的放到内存中,然后利用内存读写指令在寄存器和内存之间来回搬运数据。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烫青菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值