zoukankan      html  css  js  c++  java
  • java之数据结构与算法

    1.了解基本数据结构及特点

    如,有哪些二叉树,各有什么特点


    二叉搜索树

    每个节点都包含一个值,每个节点至多有两棵子树,左孩子小于自己,右孩子大于自己,时间复杂度是O(log(n)),随着不断插入节点,二叉树树高变大,当只有左(右)孩子时,时间复杂度变为O(n).

    平衡二叉树
    保证每个节点左右子树高度差绝对值不超过1.
    比如,AVL树在插入和删除数据是经常需要旋转以保持平衡.适合插入删除少场景.

    红黑树
    非严格平衡二叉树,更关注局部平衡,而非总体平衡,没有一条路径比其他路径长出两倍,接近平衡,减少了许多不必要的旋转操作,更加实用.
    特点:

    每个节点不是红就是黑
    根节点是黑色
    每个叶子都是黑色空节点
    红色节点的子节点都是黑的
    任意节点到其叶节点的每条路径上存在的黑色节点数量相同.

    B树
    适用于文件索引,优先减少磁盘IO次数,最大子节点称为B树的阶
    m阶b树特点:

    非叶子节点最多有m棵子树
    根节点最少有两棵子树
    非根非叶节点最少有m/2棵子树
    非子叶节点保存的关键字个数,为该节点子树个数减一
    非叶子节点的关键字大小有序
    关键字的左孩子都小于该关键字,右孩子都大于关键字
    所有叶节点都在同一层
    采用二分查找法

    B+树
    定义与b树基本相同,
    区别:

    节点有多少关键字,有多少子树
    关键字对应子树的节点都大于等于关键字,子树中包括关键字自身
    所有关键字都出现在叶节点中
    所有叶节点都有指向下一个叶节点的指针
    搜索时只会命中叶节点,叶子节点相当于数据存储层,保存关键字对应的数据,非叶节点只保存关键字与指向叶节点的指针
    B+树比B树更适合做索引:

    叶节点之间有指针相连,B+树跟适合范围检索
    由于非叶节点只保留关键字和指针,B+树可以容纳更多的关键字,降低树高,磁盘IO代价更低
    B+树查询过程稳定,必须由根节点到叶节点,所有关键字查询路径相同,效率相当.Mysql数据可得索引就提供了B+树的实现
    B*树
    在B+树的非叶节点上增加了指向同一层下一个非叶节点的指针

    2.表/栈/队列/树需要熟练掌握,深刻理解使用场景
    红黑树适合用搜索,B+数适合做索引

    数据结构
    数组/栈/队列/链表/树/哈希表/堆/图

    数组
    冒泡/选择/插入排序算法
    无序数组的优点:插入快,如果知道下标,可以很快的存取
    无序数组的缺点:查找慢,删除慢,大小固定。

    有序数组
    所谓的有序数组就是指数组中的元素是按一定规则排列的,其好处就是在根据元素值查找时可以是使用二分查找,查找效率要比无序数组高很多,在数据量很大时更加明显。当然缺点也显而易见,当插入一个元素时,首先要判断该元素应该插入的下标,然后对该下标之后的所有元素后移一位,才能进行插入,这无疑增加了很大的开销。
    因此,有序数组适用于查找频繁,而插入、删除操作较少的情况
    有序数组最大的优势就是可以提高查找元素的效率,在上例中,find方法使用了二分查找法,
    栈是后进先出,而队列刚好相反,是先进先出
    链表是一种插入和删除都比较快的数据结构,缺点是查找比较慢。除非需要频繁的通过下标来随机访问数据,否则在很多使用数组的地方都可以用链表代替
    在链表中,每个数据项都包含在“链结点”中,一个链结点是某个类的对象。每个链结点对象中都包含一个对下一个链接点的引用,链表本身的对象中有一个字段指向第一个链结点的引用,
    在数组中,每一项占用一个特定的位置,这个位置可以用一个下标号直接访问,就像一排房子,你可以凭房间号找到其中特定的意见。在链表中,寻找一个特定元素的唯一方法就是沿着这个元素的链一直找下去,知道发现要找的那个数据项

    单链表
    链表的删除指定链结点,是通过将目标链结点的上一个链结点的next指针指向目标链结点的下一个链结点,
    通过这种方法,在链表的指针链中将跳过与删除的元素,达到删除的目的。不过,实际上删除掉的元素的next指针还是指向原来的下一个元素的,只是它不能再被单链表检索到而已,JVM的垃圾回收机制会在一定的时间之后回收它
    添加链结点比删除复杂一点,首先我们要使插入位置之前的链结点的next指针指向目标链结点,其次还要将目标链结点的next指针指向插入位置之后的链结点。本例中的插入位置仅限于链表的第一个位置,

    3.了解常用的搜索/排序算法,以及复杂度和稳定性
    特别是快速排序和堆排序

    快速排序
      把整个序列看做一个数组,把第零个位置看做中轴,和最后一个比,如果比它小交换,比它大不做任何处理;交换了以后再和小的那端比,比它小不交换,比他大交换。这样循环往复,一趟排序完成,左边就是比中轴小的,右边就是比中轴大的,然后再用分治法,分别对这两个独立的数组进行排序。

    快排原理:
    在要排的数(比如数组A)中选择一个中心值key(比如A[0]),通过一趟排序将数组A分成两部分,其中以key为中心,key右边都比key大,key左边的都key小,然后对这两部分分别重复这个过程,直到整个有序。


    public int getMiddle(Integer[] list, int low, int high) {
    int tmp = list[low]; //数组的第一个作为中轴
    while (low < high) {
    while (low < high && list[high] > tmp) {
    high--;
    }
    list[low] = list[high]; //比中轴小的记录移到低端
    while (low < high && list[low] < tmp) {
    low++;
    }
    list[high] = list[low]; //比中轴大的记录移到高端
    }
    list[low] = tmp; //中轴记录到尾
    return low; //返回中轴的位置
    }


    public class QuickSort {
    public static void quickSort(int[] arr,int low,int high){
    int i,j,temp,t;
    if(low>high){
    return;
    }
    i=low;
    j=high;
    //temp就是基准位
    temp = arr[low];

    while (i<j) {
    //先看右边,依次往左递减
    while (temp<=arr[j]&&i<j) {
    j--;
    }
    //再看左边,依次往右递增
    while (temp>=arr[i]&&i<j) {
    i++;
    }
    //如果满足条件则交换
    if (i<j) {
    t = arr[j];
    arr[j] = arr[i];
    arr[i] = t;
    }

    }
    //最后将基准为与i和j相等位置的数字交换
    arr[low] = arr[i];
    arr[i] = temp;
    //递归调用左半数组
    quickSort(arr, low, j-1);
    //递归调用右半数组
    quickSort(arr, j+1, high);
    }


    public static void main(String[] args){
    int[] arr = {10,7,2,4,7,62,3,4,2,1,8,9,19};
    quickSort(arr, 0, arr.length-1);
    for (int i = 0; i < arr.length; i++) {
    System.out.println(arr[i]);
    }
    }
    }



     堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
     堆排序是一种树形选择排序方法,它的特点是:在排序的过程中,将array[0,...,n-1]看成是一颗完全二叉树的顺序存储结构,利用完全二叉树中双亲节点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(最小)的元素。


    该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

    大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]  

    小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]  

    堆排序基本思想及步骤
      堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了

    步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。

    步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

    再简单总结下堆排序的基本思路:

      a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

      b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

      c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。


    package dui;
    import java.util.Arrays;
    public class HeapSort {
    public static void main(String []args){
    int []arr = {3,1,4,2,8,5,9,7,6};
    sort(arr);
    System.out.println(Arrays.toString(arr));
    }
    public static void sort(int []arr){
    //1.构建大顶堆
    for(int i=arr.length/2-1;i>=0;i--){
    //从第一个非叶子结点从下至上,从右至左调整结构
    adjustHeap(arr,i,arr.length);
    }
    //2.调整堆结构+交换堆顶元素与末尾元素
    for(int j=arr.length-1;j>0;j--){
    swap(arr,0,j);//将堆顶元素与末尾元素进行交换
    adjustHeap(arr,0,j);//重新对堆进行调整
    }

    }

    /**
    * 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
    * @param arr
    * @param i
    * @param length
    */
    public static void adjustHeap(int []arr,int i,int length){
    int temp = arr[i];//先取出当前元素i
    for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始
    if(k+1<length && arr[k]<arr[k+1]){//如果左子结点小于右子结点,k指向右子结点
    k++;
    }
    if(arr[k] >temp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
    arr[i] = arr[k];
    i = k;
    }else{
    break;
    }
    }
    arr[i] = temp;//将temp值放到最终的位置
    }

    public static void swap(int []arr,int a ,int b){
    int temp=arr[a];
    arr[a] = arr[b];
    arr[b] = temp;
    }
    }


    4.了解常用的字符串处理算法
    Example:判断给定字符串中的符号是否匹配
    解题思路:
    1. 使用栈
    2. 遇到左括号入栈
    3. 与右括号出栈,判断出栈括号是否成对


    private static fianl Map<Character,Character> brackets = new HashMap<>();
    static{
    brackets.put(')','(');
    brackets.put(']','[');
    brackets.put('}','{');
    }
    public static boolean isMatch(String str){
    if(str==null){
    return false;
    }
    Stack<Character> stack = new stack<>();
    for(char ch : str.toCharArray()){
    if(barckets.containsValue(ch)){
    stack.put(che);
    } else if (brackets.contiansKey(ch)){
    if(stack.empty() || stack.pop() != bracjets.get(ch)){
    return false;
    }
    }
    }
    return stack.empty();
    }


    解题技巧
    认真审题:

    单模匹配还是多模匹配
    时间复杂度空间复杂度是否有要求
    明确期望的返回值,如,多匹配结果的处理
    解题思路

    单模匹配:BM,KMP算法
    多模匹配:Tire树
    前缀或后缀匹配
    可以借助栈,树等数据结构

    如,BM使用后缀匹配进行字符串匹配

    在用于查找子字符串的算法中,BM(Boyer-Moore)算法是当前有效且应用比较广泛的一种算法,各种文本编辑器的“查找”功能(Ctrl+F),大多采用Boyer-Moore算法。比我们学习的KMP算法快3~5倍。

    我们把被搜索的字符串称为文本text,待匹配的字符串称为模式串pattern。BM算法的核心思想就是两个,第一是坏字符,第二是好后缀,好后缀就是pattern与text从右往左连续匹配成功的子串,坏字符就是pattern与text从右往左第一个匹配失败的在text中的字符,

    对于坏字符和好后缀,无非就是不同的模式串移动规则,通过各自不同的移动规则,确定分别对于坏字符和好后缀模式串需要移动的位数,最终选择移动位数最大的进行移动即可。

    一般来说,文本和模式串都是用数组来表示的,在字符串的匹配过程中,当出现不匹配时,需要进行模式串相对于文本的移动,我们简称为模式串的移动。

    1、坏字符规则

    后移位数 = 坏字符的位置 - 模式串中的坏字符上一次出现位置

    如果"坏字符"不包含在模式串之中,则上一次出现位置为 -1。

    2、好后缀规则

    后移位数 = 好后缀的位置 - 模式串中的上一次出现位置

    package com.buaa;

    import java.util.Random;

    /**
    * @ProjectName StringPatternMatchAlgorithm
    * @PackageName com.buaa
    * @ClassName BM
    * @Description TODO
    */
    public class BM {
    /**
    * 利用坏字符规则计算移动位数
    */
    public static int badCharacter(String moduleString, char badChar,int badCharSuffix){
    return badCharSuffix - moduleString.lastIndexOf(badChar, badCharSuffix);
    }

    /**
    * 利用好后缀规则计算移动位数
    */
    public static int goodCharacter(String moduleString,int goodCharSuffix){
    int result = -1;
    // 模式串长度
    int moduleLength = moduleString.length();
    // 好字符数
    int goodCharNum = moduleLength -1 - goodCharSuffix;

    for(;goodCharNum > 0; goodCharNum--){
    String endSection = moduleString.substring(moduleLength - goodCharNum, moduleLength);
    String startSection = moduleString.substring(0, goodCharNum);
    if(startSection.equals(endSection)){
    result = moduleLength - goodCharNum;
    }
    }

    return result;
    }

    /**
    * BM匹配字符串
    *
    * @param originString 主串
    * @param moduleString 模式串
    * @return 若匹配成功,返回下标,否则返回-1
    */
    public static int match(String originString, String moduleString){
    // 主串
    if (originString == null || originString.length() <= 0) {
    return -1;
    }
    // 模式串
    if (moduleString == null || moduleString.length() <= 0) {
    return -1;
    }
    // 如果模式串的长度大于主串的长度,那么一定不匹配
    if (originString.length() < moduleString.length()) {
    return -1;
    }

    int moduleSuffix = moduleString.length() -1;
    int module_index = moduleSuffix;
    int origin_index = moduleSuffix;

    for(int ot = origin_index; origin_index < originString.length() && module_index >= 0;){
    char oc = originString.charAt(origin_index);
    char mc = moduleString.charAt(module_index);
    if(oc == mc){
    origin_index--;
    module_index--;
    }else{
    // 坏字符规则
    int badMove = badCharacter(moduleString,oc,module_index);
    // 好字符规则
    int goodMove = goodCharacter(moduleString,module_index);
    // 下面两句代码可以这样理解,主串位置不动,模式串向右移动
    origin_index = ot + Math.max(badMove, goodMove);
    module_index = moduleSuffix;
    // ot就是中间变量
    ot = origin_index;
    }
    }

    if(module_index < 0){
    // 多减了一次
    return origin_index + 1;
    }

    return -1;
    }

    /**
    * 随机生成字符串
    *
    * @param length 表示生成字符串的长度
    * @return String
    */
    public static String generateString(int length) {
    String baseString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

    StringBuilder result = new StringBuilder();

    Random random = new Random();
    for (int i = 0; i < length; i++) {
    result.append(baseString.charAt(random.nextInt(baseString.length())));
    }

    return result.toString();
    }

    public static void main(String[] args) {
    // 主串
    // String originString = generateString(10);
    String originString = "HERE IS A SIMPLE EXAMPLE";
    // 模式串
    // String moduleString = generateString(4);
    String moduleString = "EXAMPLE";
    // 坏字符规则表
    // int[] badCharacterArray = badCharacter(originString,moduleString);

    System.out.println("主串:" + originString);
    System.out.println("模式串:" + moduleString);


    int index = match(originString, moduleString);
    System.out.println("匹配的下标:" + index);
    }
    }


    5.能够分析算法实现的复杂度
    特别是时间复杂度

    O(1)
    O(1)是常量级时间复杂度的一种表示方法,并非只执行一行代码
    代码执行时间不是随着n的增大而增大,这样的代码的时间复杂度都是O(1)
    通常只要算法中不存在循环、递归,即使代码有很多行,时间复杂度仍是O(1)

    ②O(logn)、O(nlogn)对数阶时间复杂度
    这段代码的第3行是执行次数最多的,只要算出第3行执行的次数,就是整个代码的时间复杂度。
    i从1开始取值,每一次循环乘以2.可以看到 i=i*2是一个等比数列

    我们只要算出x是多少,就是执行的次数了  2^x=n -->x=log2n,所以时间复杂度应该为O(log2n)

    很容易就能看出来,应该是O(log3n)。
    但是上面的O(log2n)和O(log3n)可以通过换底公式换成以2为底的对数,且可以忽略系数,所以都记做 O(logn)。 
    关于O(nlogn),就是把上面的代码在循环执行n遍了。其中归并排序、快速排序的时间复杂度就是O(nlogn)

    ③O(m+n)、O(m*n)
    1. 加法法则(量级最大法则):总复杂度等于量级最大的那段代码的复杂度
    同理,sum_2和sum_3分别是 O(n)和O(n^2),对于这三个,我们取量级最大的O(n^2),所以总的时间复杂度就等于量级最大的那段代码的时间复杂度。

    2.乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积 
    f()函数的时间复杂度是 T1(n)=O(n),如果先把f()函数看成简单的操作,则cal()函数的时间复杂度是T2(n)=O(n),所以整个cal()函数的时间复杂度是T(n)=T2(n)*T1(n)=O(n*n)=O(n^2) 

    一个经验规则:
    其中c是一个常量,如果一个算法的复杂度为c 、 log2n 、n 、 nlog2   、  n* ,那么这个算法时间效率比较高 ,
    如果是2n** ,3n** ,n!,那么稍微大一些的n就会令这个算法不能动了,居于中间的几个则差强人意。

    TopK问题
    找出N个数中最小的k个数(N非常大)
    解法:

    用前K个数创建大小为K的大根堆
    剩余的N-K个数与堆顶进行比较
    时间复杂度:O(N*log(K))
    优点:不用在内存中读入所有元素,适用于非常大的数据集

    从N有序队列中找到最小的K个值
    解法:

    用N个队列的最小值组成大小为K的小根堆
    取堆顶值
    将堆顶值所在队列的下个值加入堆(与堆中最大值比较,若该值大于最大值则可停止循环)
    重复步骤2,直到K次
    时间复杂度:O((N+K-1)*log(K))


    1.能够将数据结构与实际使用场景结合
    如,介绍红黑树时,结合TreeMap的实现,介绍B+数时,结合Mysql中的索引实现

    TreeMap是基于红黑树的实现,也是记录了key-value的映射关系,该映射根据key的自然排序进行排序或者根据构造方法中传入的比较器进行排序,也就是说TreeMap是有序的key-value集合。
    通过TreeMap的定义可以看出以下几点:
    1.TreeMap的内部实现是红黑树实现的,
    2.TreeMap是有序的key-value集合。

    mysql的B+树 索引

    7.mysql索引底层数据结构与算法
    索引的数据结构 二叉树/HASH/BTREE
    Btree 度(Degree)-节点的数据存储个数 横向变长,高度变少,节点查找是在内存里
    一次IO是4K数据,节点的度就是4K数据
    B+Tree 非叶子节点不存储data,只存储key,可以增大度
    一般使用磁盘IO次数评价索引结构的优劣
    myisam索引实现 存储引擎是表级别
    索引和数据是分离的 叶子节点存的是文件指针不是数据
    主键索引/非主键索引/分开存储的
    innodb 主键索引 数据文件本身就是索引文件 叶子节点存储就是数据
    innodb必须要有主键 整型自增主键
    非主键索引叶子节点存储的是主键,并不是数据,需要查找2次才能找到数据
    联合索引的底层存储结构同上

    真题汇总
    1.各种排序算法的实现和复杂度,稳定性
    这个有个表
    2.二叉树的前中后序遍历
    1.前序遍历
        前序遍历(DLR,lchild,data,rchild),是二叉树遍历的一种,也叫做先根遍历、先序遍历、前序周游,可记做根左右。前序遍历首先访问根结点然后遍历左子树,最后遍历右子树。

    前序遍历首先访问根结点然后遍历左子树,最后遍历右子树。在遍历左、右子树时,仍然先访问根结点,然后遍历左子树,最后遍历右子树。
    若二叉树为空则结束返回,否则:
    (1)访问根结点。
    (2)前序遍历左子树。
    (3)前序遍历右子树 。前序遍历
    需要注意的是:遍历左右子树时仍然采用前序遍历方法。
    如右图所示二叉树
    前序遍历结果:ABDECF
    已知后序遍历和中序遍历,就能确定前序遍历。
        其实在遍历二叉树的时候有三次遍历, 比如前序遍历:A->B->D->D(D左子节点并返回到D)->D(D右子节点并返回到D)->B->E->E(左)->E(右)->->B->A->C->F->F(左)->F(右)->C->C(右),所以可以用栈结构,把遍历到的节点压进栈,没子节点时再出栈。也可以用递归的方式,递归的输出当前节点,然后递归的输出左子节点,最后递归的输出右子节点。直接看代码更能理解:

    package test;
    //前序遍历的递归实现与非递归实现
    import java.util.Stack;
    public class Test
    {
    public static void main(String[] args)
    {
    TreeNode[] node = new TreeNode[10];//以数组形式生成一棵完全二叉树
    for(int i = 0; i < 10; i++)
    {
    node[i] = new TreeNode(i);
    }
    for(int i = 0; i < 10; i++)
    {
    if(i*2+1 < 10)
    node[i].left = node[i*2+1];
    if(i*2+2 < 10)
    node[i].right = node[i*2+2];
    }

    preOrderRe(node[0]);
    }

    public static void preOrderRe(TreeNode biTree)
    {//递归实现
    System.out.println(biTree.value);
    TreeNode leftTree = biTree.left;
    if(leftTree != null)
    {
    preOrderRe(leftTree);
    }
    TreeNode rightTree = biTree.right;
    if(rightTree != null)
    {
    preOrderRe(rightTree);
    }
    }

    public static void preOrder(TreeNode biTree)
    {//非递归实现
    Stack<TreeNode> stack = new Stack<TreeNode>();
    while(biTree != null || !stack.isEmpty())
    {
    while(biTree != null)
    {
    System.out.println(biTree.value);
    stack.push(biTree);
    biTree = biTree.left;
    }
    if(!stack.isEmpty())
    {
    biTree = stack.pop();
    biTree = biTree.right;
    }
    }
    }
    }

    class TreeNode//节点结构
    {
    int value;
    TreeNode left;
    TreeNode right;

    TreeNode(int value)
    {
    this.value = value;
    }
    }


    2.中序遍历
    中序遍历(LDR)是二叉树遍历的一种,也叫做中根遍历、中序周游。在二叉树中,先左后根再右。巧记:左根右。
    中序遍历首先遍历左子树,然后访问根结点,最后遍历右子树
    若二叉树为空则结束返回,
    否则:

      (1)中序遍历左子树
    (2)访问根结点
    (3)中序遍历右子树
    如右图所示二叉树
    中序遍历结果:DBEAFC

    import java.util.Stack;
    public class Test
    {
    public static void main(String[] args)
    {
    TreeNode[] node = new TreeNode[10];//以数组形式生成一棵完全二叉树
    for(int i = 0; i < 10; i++)
    {
    node[i] = new TreeNode(i);
    }
    for(int i = 0; i < 10; i++)
    {
    if(i*2+1 < 10)
    node[i].left = node[i*2+1];
    if(i*2+2 < 10)
    node[i].right = node[i*2+2];
    }

    midOrderRe(node[0]);
    System.out.println();
    midOrder(node[0]);
    }

    public static void midOrderRe(TreeNode biTree)
    {//中序遍历递归实现
    if(biTree == null)
    return;
    else
    {
    midOrderRe(biTree.left);
    System.out.println(biTree.value);
    midOrderRe(biTree.right);
    }
    }


    public static void midOrder(TreeNode biTree)
    {//中序遍历费递归实现
    Stack<TreeNode> stack = new Stack<TreeNode>();
    while(biTree != null || !stack.isEmpty())
    {
    while(biTree != null)
    {
    stack.push(biTree);
    biTree = biTree.left;
    }
    if(!stack.isEmpty())
    {
    biTree = stack.pop();
    System.out.println(biTree.value);
    biTree = biTree.right;
    }
    }
    }
    }

    class TreeNode//节点结构
    {
    int value;
    TreeNode left;
    TreeNode right;

    TreeNode(int value)
    {
    this.value = value;
    }
    }


    3.后序遍历(难点)
    后序遍历(LRD)是二叉树遍历的一种,也叫做后根遍历、后序周游,可记做左右根。后序遍历有递归算法和非递归算法两种。在二叉树中,先左后右再根。巧记:左右根。
    后序遍历首先遍历左子树,然后遍历右子树,最后访问根结点,在遍历左、右子树时,仍然先遍历左子树,然后遍历右子树,最后遍历根结点。即:
    若二叉树为空则结束返回,
    否则:(1)后序遍历左子树
    (2)后序遍历右子树
    (3)访问根结点
    如右图所示二叉树
    后序遍历结果:DEBFCA
    已知前序遍历和中序遍历,就能确定后序遍历。
    算法核心思想:
        首先要搞清楚先序、中序、后序的非递归算法共同之处:用栈来保存先前走过的路径,以便可以在访问完子树后,可以利用栈中的信息,回退到当前节点的双亲节点,进行下一步操作。
        后序遍历的非递归算法是三种顺序中最复杂的,原因在于,后序遍历是先访问左、右子树,再访问根节点,而在非递归算法中,利用栈回退到时,并不知道是从左子树回退到根节点,还是从右子树回退到根节点,如果从左子树回退到根节点,此时就应该去访问右子树,而如果从右子树回退到根节点,此时就应该访问根节点。所以相比前序和后序,必须得在压栈时添加信息,以便在退栈时可以知道是从左子树返回,还是从右子树返回进而决定下一步的操作。

    import java.util.Stack;
    public class Test
    {
    public static void main(String[] args)
    {
    TreeNode[] node = new TreeNode[10];//以数组形式生成一棵完全二叉树
    for(int i = 0; i < 10; i++)
    {
    node[i] = new TreeNode(i);
    }
    for(int i = 0; i < 10; i++)
    {
    if(i*2+1 < 10)
    node[i].left = node[i*2+1];
    if(i*2+2 < 10)
    node[i].right = node[i*2+2];
    }

    postOrderRe(node[0]);
    System.out.println("***");
    postOrder(node[0]);
    }

    public static void postOrderRe(TreeNode biTree)
    {//后序遍历递归实现
    if(biTree == null)
    return;
    else
    {
    postOrderRe(biTree.left);
    postOrderRe(biTree.right);
    System.out.println(biTree.value);
    }
    }

    public static void postOrder(TreeNode biTree)
    {//后序遍历非递归实现
    int left = 1;//在辅助栈里表示左节点
    int right = 2;//在辅助栈里表示右节点
    Stack<TreeNode> stack = new Stack<TreeNode>();
    Stack<Integer> stack2 = new Stack<Integer>();//辅助栈,用来判断子节点返回父节点时处于左节点还是右节点。

    while(biTree != null || !stack.empty())
    {
    while(biTree != null)
    {//将节点压入栈1,并在栈2将节点标记为左节点
    stack.push(biTree);
    stack2.push(left);
    biTree = biTree.left;
    }

    while(!stack.empty() && stack2.peek() == right)
    {//如果是从右子节点返回父节点,则任务完成,将两个栈的栈顶弹出
    stack2.pop();
    System.out.println(stack.pop().value);
    }

    if(!stack.empty() && stack2.peek() == left)
    {//如果是从左子节点返回父节点,则将标记改为右子节点
    stack2.pop();
    stack2.push(right);
    biTree = stack.peek().right;
    }

    }
    }
    }

    class TreeNode//节点结构
    {
    int value;
    TreeNode left;
    TreeNode right;

    TreeNode(int value)
    {
    this.value = value;
    }
    }


    4.层次遍历
        与树的前中后序遍历的DFS思想不同,层次遍历用到的是BFS思想。一般DFS用递归去实现(也可以用栈实现),BFS需要用队列去实现。
    层次遍历的步骤是:
        1.对于不为空的结点,先把该结点加入到队列中
        2.从队中拿出结点,如果该结点的左右结点不为空,就分别把左右结点加入到队列中
        3.重复以上操作直到队列为空

    public static void levelOrder(TreeNode biTree)
    {//层次遍历
    if(biTree == null)
    return;
    LinkedList<TreeNode> list = new LinkedList<TreeNode>();
    list.add(biTree);
    TreeNode currentNode;
    while(!list.isEmpty())
    {
    currentNode = list.poll();
    System.out.println(currentNode.value);
    if(currentNode.left != null)
    list.add(currentNode.left);
    if(currentNode.right != null)
    list.add(currentNode.right);
    }
    }
    先序遍历特点:第一个值是根节点
    中序遍历特点:根节点左边都是左子树,右边都是右子树


    3.翻转句子中单词的顺序
    定义两个指针,依次交换对应的字符串,即可

    例如 str = “I am a student.”,array = {“I”,“am”, “a”, “student.”}

    array[0]和array[3]交换,{“student.”, “am”, “a”, “I”}
    array[1]和array[2]交换,{“student.”, “a”, “am”, “I”}
    public static String reverseStringSequence(String str) {
    if (Strings.isNullOrEmpty(str)) {
    return str;
    }
    String[] seq = str.split(" ");
    // 定义两个指针,一个从头开始,一个从尾开始,成对交换,当两个指针相遇时则停止
    int start = 0;
    int end = seq.length - 1;
    while (start < end) {
    String temp = seq[start];
    seq[start] = seq[end];
    seq[end] = temp;
    start++;
    end--;
    }
    return StringUtils.join(seq, " ");
    }

    public static void main(String[] args) {;
    String result = reverseStringSequence("I am a student.");
    System.out.println(result);
    }
    解法二
    思路和上面一样,都是字符串反转,这里不以单词为单位,而是以字符为单位,所以需要进行两步反转

    对每个单词进行反转得到"I ma a .tneduts"
    反转整个字符串得到"student. a am I"
    public static String reverseStringSequence(String str) {
    if (Strings.isNullOrEmpty(str)) {
    return str;
    }
    char[] seq = str.toCharArray();
    int length = seq.length;
    // 定义两个指针记录要反转单词的起始位置
    int start = 0;
    int end = 0;
    // 这里一定要含有等于,因为要判断是否是最后一个单词,从而可以处理最后一个单词
    while (end <= length) {
    // 当已经遍历到字符串的最后一个字符,或者当前字符是空格时
    // 则对空格前的单词进行反转,即"am"反转为"ma"
    // 一定要把判断是否是结尾放在前面,否则seq[end]会报错,因为数组的有效索引是从0开始的
    // 反转后修改单词的起始指针为空格的下一个字符
    // 如果不符合条件,则移动指针继续判断下一个字符
    if (end == length || seq[end] == ' ') {
    reverse(seq, start, end - 1);
    start = end + 1;
    }
    end++;
    }
    // 反转这个数组
    reverse(seq, 0, length - 1);
    return new String(seq);
    }

    private static void reverse(char[] seq, int start, int end) {
    while (start < end) {
    char temp = seq[start];
    seq[start] = seq[end];
    seq[end] = temp;
    start++;
    end--;
    }
    }


    4.用栈模拟队列(或用队列模拟栈)
    问题描述
    用两个栈实现队列
    用两个队列实现栈
    问题分析
    用两个栈实现队列
    stackPush 用作 Push 操作,Push 是直接 push 进这个栈。
    stackPop 用作 Pop 操作,若 stackPop 当前不为空,那么直接 pop ,若为空,那么将 stackPush 全部倒入 stackPop 中,然后 stackPop 再 pop 出一个元素
    用两个队列实现栈
    用两个队列 queue 队列 与 help 队列
    Push 时直接 push 进 queue队列
    Pop 时先检查queue队列是否为空,若为空,说明所要实现的栈中不存在元素。若不为空,则将 queue队列中的元素装入 help中,queue中只剩最后一个,那么这便是应该Pop的元素,将该元素pop。pop之后,交换两队列,原help队列作现queue,原queue作现help即可。
    代码实现
    package basic_class_03;

    import java.util.LinkedList;
    import java.util.Queue;
    import java.util.Stack;


    public class MyStackAndQueueConvert {
    //用两个栈实现队列
    public static class TwoStackQueue{
    private Stack<Integer> stackPush;
    private Stack<Integer> stackPop;

    public TwoStackQueue() {
    this.stackPop = new Stack<>();
    this.stackPush = new Stack<>();
    }

    public void push (int pushInt) {
    stackPush.push(pushInt);
    }

    public int poll() {
    if (stackPop.isEmpty() && stackPush.isEmpty()) {
    throw new RuntimeException("Queue is empty!");
    }else if (stackPop.isEmpty()) {
    while (! stackPush.isEmpty()) {
    stackPop.push(stackPush.pop());
    }
    }
    return stackPop.pop();
    }

    public int peek() {
    if (stackPop.isEmpty() && stackPush.isEmpty()) {
    throw new RuntimeException("Queue is empty!");
    }else if (stackPop.isEmpty()) {
    while (! stackPush.isEmpty()) {
    stackPop.push(stackPop.pop());
    }
    }
    return stackPop.peek();
    }
    }


    //用两个队列实现栈
    public static class TwoQueuesStack{
    private Queue<Integer> queue;
    private Queue<Integer> help;

    public TwoQueuesStack() {
    queue = new LinkedList<>();
    help = new LinkedList<>();
    }

    public void push(int pushInt) {
    queue.add(pushInt);
    }

    public int peek() {
    if (queue.isEmpty()) {
    throw new RuntimeException("Stack is empty!");
    }
    while (queue.size() != 1) {
    help.add(queue.poll());
    }
    int res = queue.poll();
    help.add(res);
    swap();
    return res;
    }

    public int pop() {
    if (queue.isEmpty()) {
    throw new RuntimeException("Stack is empty!");
    }
    while (queue.size() != 1) {
    help.add(queue.poll());
    }
    int res = queue.poll();
    swap();
    return res;
    }

    public void swap() {
    Queue<Integer> temp = queue;
    queue = help;
    help = temp;
    }

    }

    }


    5.对10亿个数进行排序,限制内存为1G
    采用分治的思路
    3、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
    方案:顺序读文件中,对于每个词x,取hash(x)%5000,然后按照该值存到5000个小文件(记为x0,x1,…x4999)中。这样每个文件大概是200k左右。
    如果其中的有的文件超过了1M大小,还可以按照类似的方法继续往下分,直到分解得到的小文件的大小都不超过1M。
    对每个小文件,统计每个文件中出现的词以及相应的频率(可以采用trie树/hash_map等),并取出出现频率最大的100个词(可以用含100个结 点的最小堆),并把100个词及相应的频率存入文件,这样又得到了5000个文件。下一步就是把这5000个文件进行归并(类似与归并排序)的过程了。

    6.去掉(或找出)两个数组中重复的数
    排序和hash两种思路
    在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

            因为数组中的元素都是0到n-1的,所以数组的值可以作为数组的下标,hash数组原始都为为,遍历原数组,取到的值作为index,判断在hash数组中这个 index数字对应的值,如果是0,还没有出现过,赋值为1,如果是1了,就说明出现过了,返回现在取到的numbers数组的值

    public class Solution {
    // Parameters:
    // numbers: an array of integers
    // length: the length of array numbers
    // duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation;
    // Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++
    // 这里要特别注意~返回任意重复的一个,赋值duplication[0]
    // Return value: true if the input is valid, and there are some duplications in the array number
    // otherwise false
    public boolean duplicate(int numbers[],int length,int [] duplication) {
    int hash[]=new int[length];
    for(int i=0;i<length;i++){
    if(hash[numbers[i]]==0){
    hash[numbers[i]]++;
    }
    else{
    duplication[0]=numbers[i];
    return true;
    }
    }
    return false;
    }
    }

    public static int[] distinct(int[] arr){
    int length=1;
    boolean isExist=false;
    for(int i=1;i<arr.length;i++){
    for(int j=0;j<length;j++){
    if(arr[i]==arr[j]){
    isExist=true;
    break;
    }
    }
    if(!isExist){
    arr[length]=arr[i];
    length++;
    }
    isExist=false;
    }
    int[] newArr=new int[length];
    System.arraycopy(arr, 0, newArr, 0, length);
    return newArr;
    }


    7.将一颗二叉树转换成其镜像

    package tree;

    public class MirrorTree {

    /**
    * 将一颗二叉树转换成它的镜像
    * @param args
    */
    public static void convertmirror(TreeNode root){
    if(root==null||(root.left==null&&root.right==null)) return;
    TreeNode temp = root.left;
    root.left = root.right;
    root.right = temp;
    convertmirror(root.left);
    convertmirror(root.right);
    }
    public static void printf(TreeNode root){
    if(root==null) return;
    System.out.print(root.value+" ");
    printf(root.left);
    printf(root.right);
    }
    public static void main(String[] args) {

    TreeNode root = new TreeNode(1);
    root.left = new TreeNode(3);
    root.right = new TreeNode(2);
    root.right.left = new TreeNode(5);
    root.right.right = new TreeNode(4);
    printf(root);
    System.out.println();
    convertmirror(root);
    printf(root);

    }

    }

    8.确定一个字符串中的括号是否匹配
    1、将字符串的每个字符进行遍历
    2、如果发现是左括号,那么将该字符压入到栈中
    3、如果是右括号,先去存储好的栈顶找到相应的值
    4、若栈为空返回false,若匹配,pop该左括号,若不匹配也返回false
    5、最后看存储栈中的做括号是否都匹配上了,也就是栈最后为空,返回true,否则返回false

    package Algro;

    import java.util.HashMap;
    import java.util.Map;
    import java.util.Stack;

    public class Match {

    static boolean isMatch(String s){
    //定义左右括号的对应关系
    Map<Character,Character> bracket = new HashMap<>();
    bracket.put(')','(');
    bracket.put(']','[');
    bracket.put('}','{');

    Stack stack = new Stack();

    for(int i = 0; i < s.length(); i++){

    Character temp = s.charAt(i);//先转换成字符
    //是否为左括号
    if(bracket.containsValue(temp)){

    stack.push(temp);
    //是否为右括号
    }else if(bracket.containsKey(temp)){
    if(stack.isEmpty()){
    return false;
    }
    //若左右括号匹配
    if(stack.peek() == bracket.get(temp)){
    stack.pop();
    }
    else{
    return false;
    }
    }

    }
    return stack.isEmpty()? true: false;

    }


    public static void main(String[] args) {

    System.out.println(isMatch("(***)-[{-------}]")); //true
    System.out.println(isMatch("(2+4)*a[5]")); //true
    System.out.println(isMatch("({}[]]])")); //false
    System.out.println(isMatch("())))")); //false

    }
    }

    import java.util.HashMap;
    import java.util.Map;
    import java.util.Stack;

    public class BracketMatching {
    // pair以右括号为key, 左括号为值
    private Map<Character, Character> pair = null;

    public BracketMatching()
    {
    pair = new HashMap<Character, Character>();
    pair.put(')', '(');
    pair.put('}', '{');
    pair.put(']', '[');
    }

    public boolean isMatch(String s)
    {
    Stack<Character> sc = new Stack<Character>();
    for (int i = 0; i < s.length(); i++)
    {
    Character ch = s.charAt(i);
    if (pair.containsValue(ch))// 如果是左括号,放入栈中
    {
    sc.push(ch);
    } else if (pair.containsKey(ch)) // 如果是右括号
    {
    if (sc.empty()) // 栈为空,栈头没有字符与右括号匹配
    {
    return false;
    }
    // 栈不为空,栈头字符与右括号匹配
    if (sc.peek() == pair.get(ch))
    {
    sc.pop();
    } else //网上许多列子没有这里的else代码块,导致({}[]]])会被判断为true
    { // 栈不为空,栈头字符不与右括号匹配
    return false;
    }
    }

    }

    return sc.empty() ? true : false;
    }

    public static void main(String[] args)
    {
    BracketMatching judger = new BracketMatching();
    System.out.println(judger.isMatch("(***)-[{-------}]")); //true
    System.out.println(judger.isMatch("(2+4)*a[5]")); //true
    System.out.println(judger.isMatch("({}[]]])")); //false
    System.out.println(judger.isMatch("())))")); //false
    System.out.println(judger.isMatch("((()")); //false
    System.out.println(judger.isMatch("(){[[]]}")); //true
    }


    }


    9.给定一个开始词,一个结束词,一个字典,如何找到从开始词到结束词的最短单词接龙
    考虑使用深度优先算法

    给定的俩个字串和字典中的串是一张无向图,构造一棵图的树,广度遍历(按层)这棵树,每次改变开始串的一个字符,如果在字典中就入队列,并从字典中删除,防止重复。

    int ladderLength(string start, string end, unordered_set<string> &dict) {
    int count=1;
    unordered_set<string> ret=dict;
    queue<string> que;
    que.push(start);
    while(!que.empty()){
    int size=que.size();
    //构造树当前层循环
    while(size--){
    string s=que.front();
    que.pop();
    for(int i=0;i<s.length();i++){
    string word=s;
    for(char j='a';j<='z';j++){
    if(s[i]==j)
    continue;
    s[i]=j;
    if(s==end)
    return count+1;
    if(ret.count(s)>0){
    que.push(s);
    ret.erase(s);
    }
    }
    s=word; //还原字符串
    }
    }
    //遍历完一层 加 1
    count++;
    }
    return 0;
    }


    10.如何查找两个二叉树的最近公共祖先
    递归与非递归皆可实现

    根据Wikipedia对LCA的定义:“在两个节点p和q之间定义的最低公共祖先是T中同时具有p和q作为后代的最低节点(在这里,我们允许一个节点作为其自身的后代)。”

    解决
    与找二叉搜索树的最近公共祖先类似,如果一个节点的左子树上有与p或q相等的节点且右子树上有与p或q相等的节点,说明此时该节点即为最近公共节点

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    //递归出口
    if(root == null || root == p || root ==q)
    return root;
    //去该节点的左子树上找
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    //去该节点的右子树上找
    TreeNode right = lowestCommonAncestor(root.right, p, q);
    if(left == null){
    //左子树上没有,说明在右子树上
    return right;
    }else if(right == null){
    //右子树上没有,说明在左子树上
    return left;
    }
    //左右均有,说明该节点即为最近公共祖先
    return root;
    }

  • 相关阅读:
    操作系统——理论知识
    BEGIN-4 Fibonacci数列
    BEGIN-3 圆的面积
    面向对象三大特征之一:多态
    面向对象三大特征之二:继承
    package---包
    面向对象三大特征之一:封装
    关键字:This(上)
    无参构造与有参构造
    面向对象
  • 原文地址:https://www.cnblogs.com/muzinan110/p/11105693.html
Copyright © 2011-2022 走看看