数据结构基础

一. 概述

1. 理解

1.1 数据结构与算法的关系

数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构。

程序 = 数据结构 + 算法

数据结构是算法的基础

1.2 线性结构和非线性结构

线性结构

  • 作为最常用的数据结构,特点是数据元素之间存在一对一的线性关系。

  • 包含两种不同的存储结构:顺序存储结构(如数组) 和 链式存储结构(如链表)。

  • 顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的。

  • 链式存储的线性表称为链表,链表的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息。

  • 线性结构常见的有:数组,链表,栈,队列,哈希表(散列表)。

非线性结构

  • 树形结构:二叉树,AVL树,红黑树,B树,堆,Trie,哈夫曼树,并查集...
  • 图形结构:邻接矩阵,邻接表...

2. 代码测试工具

2.1 测试某段代码的运行时间

public class TimeUtils {
	private static final SimpleDateFormat fmt = new SimpleDateFormat("HH:mm:ss.SSS");
	
	public interface Task {
		void execute();
	}
	
	public static void test(String title, Task task) {
		if (task == null) return;
		title = (title == null) ? "" : ("【" + title + "】");
		System.out.println(title);
		System.out.println("开始:" + fmt.format(new Date()));
		long begin = System.currentTimeMillis();
		task.execute();
		long end = System.currentTimeMillis();
		System.out.println("结束:" + fmt.format(new Date()));
		double delta = (end - begin) / 1000.0;
		System.out.println("耗时:" + delta + "秒");
		System.out.println("-------------------------------------");
	}
}

2.2 断言工具

public class Asserts {
	public static void test(boolean value) {
		try {
			if (!value) throw new Exception("测试未通过");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

2.3 Integer工具

public class IntegerUtils {
	/** 生成随机数 */
	public static Integer[] random(int count, int min, int max) {
		if (count <= 0 || min > max) return null;
		Integer[] array = new Integer[count];
		int delta = max - min + 1;
		for (int i = 0; i < count; i++) {
			array[i] = min + (int)(Math.random() * delta);
		}
		return array;
	}

	/** 合并两个数组 */
	public static Integer[] combine(Integer[] array1, Integer[] array2) {
		if (array1 == null || array2 == null) return null;
		Integer[] array = new Integer[array1.length + array2.length];
		for (int i = 0; i < array1.length; i++) {
			array[i] = array1[i];
		}
		for (int i = 0; i < array2.length; i++) {
			array[i + array1.length] = array2[i];
		}
		return array;
		
	}

	public static Integer[] same(int count, int unsameCount) {
		if (count <= 0 || unsameCount > count) return null;
		Integer[] array = new Integer[count];
		for (int i = 0; i < unsameCount; i++) {
			array[i] = unsameCount - i;
		}
		for (int i = unsameCount; i < count; i++) {
			array[i] = unsameCount + 1;
		}
		return array;
	}

	/**
	 * 生成头部和尾部是升序的数组
	 * disorderCount:希望多少个数据是无序的
	 */
	public static Integer[] headTailAscOrder(int min, int max, int disorderCount) {
		Integer[] array = ascOrder(min, max);
		if (disorderCount > array.length) return array;
		
		int begin = (array.length - disorderCount) >> 1;
		reverse(array, begin, begin + disorderCount);
		return array;
	}

	/**
	 * 生成中间是升序的数组
	 * disorderCount:希望多少个数据是无序的
	 */
	public static Integer[] centerAscOrder(int min, int max, int disorderCount) {
		Integer[] array = ascOrder(min, max);
		if (disorderCount > array.length) return array;
		int left = disorderCount >> 1;
		reverse(array, 0, left);
		
		int right = disorderCount - left;
		reverse(array, array.length - right, array.length);
		return array;
	}

	/**
	 * 生成头部是升序的数组
	 * disorderCount:希望多少个数据是无序的
	 */
	public static Integer[] headAscOrder(int min, int max, int disorderCount) {
		Integer[] array = ascOrder(min, max);
		if (disorderCount > array.length) return array;
		reverse(array, array.length - disorderCount, array.length);
		return array;
	}

	/**
	 * 生成尾部是升序的数组
	 * disorderCount:希望多少个数据是无序的
	 */
	public static Integer[] tailAscOrder(int min, int max, int disorderCount) {
		Integer[] array = ascOrder(min, max);
		if (disorderCount > array.length) return array;
		reverse(array, 0, disorderCount);
		return array;
	}

	/** 升序生成数组 */
	public static Integer[] ascOrder(int min, int max) {
		if (min > max) return null;
		Integer[] array = new Integer[max - min + 1];
		for (int i = 0; i < array.length; i++) {
			array[i] = min++;
		}
		return array;
	}

	/** 降序生成数组 */
	public static Integer[] descOrder(int min, int max) {
		if (min > max) return null;
		Integer[] array = new Integer[max - min + 1];
		for (int i = 0; i < array.length; i++) {
			array[i] = max--;
		}
		return array;
	}
	
	/** 反转数组 */
	private static void reverse(Integer[] array, int begin, int end) {
		int count = (end - begin) >> 1;
		int sum = begin + end - 1;
		for (int i = begin; i < begin + count; i++) {
			int j = sum - i;
			int tmp = array[i];
			array[i] = array[j];
			array[j] = tmp;
		}
	}

	/** 复制数组 */
	public static Integer[] copy(Integer[] array) {
		return Arrays.copyOf(array, array.length);
	}

	/** 判断数组是否升序 */
	public static boolean isAscOrder(Integer[] array) {
		if (array == null || array.length == 0) return false;
		for (int i = 1; i < array.length; i++) {
			if (array[i - 1] > array[i]) return false;
		}
		return true;
	}

	/** 打印数组 */
	public static void println(Integer[] array) {
		if (array == null) return;
		StringBuilder string = new StringBuilder();
		for (int i = 0; i < array.length; i++) {
			if (i != 0) string.append("_");
			string.append(array[i]);
		}
		System.out.println(string);
	}
}

二. 复杂度

1. 算法的效率问题

使用不同算法,解决同一个问题,效率可能相差非常大

1.1 求第n个斐波拉契数

  • 斐波那契数列的排列是:0,1,1,2,3,5,8,13,21,34,55,89,144...

  • 它后一个数等于前面两个数的和

image-20210801170249179
public class FibonacciNumber {
    public static void main(String[] args) {
        //耗时:4.674秒
        TimeUtils.test("求第n个斐波那契数:fib1", new TimeUtils.Task() {
            @Override
            public void execute() {
                System.out.println(fib1(45));
            }
        });

        //耗时:0.0秒
        TimeUtils.test("求第n个斐波那契数:fib2", new TimeUtils.Task() {
            @Override
            public void execute() {
                System.out.println(fib2(45));
            }
        });
    }

    /**
     * 实现一:递归
     * 时间复杂度:O(2^n)
     */
    public static int fib1(int n) {
        if (n <= 1) return n;
        return fib1(n - 1) + fib1(n - 2);
    }

    /**
     * 实现二:循环
     * 时间复杂度:O(n)
     * <p>
     * 0,1,2,3,4,5,6
     * 0,1,1,2,3,5,8,13
     */
    public static int fib2(int n) {
        if (n <= 1) return n;
        int first = 0, second = 1;
//            int sum = first + second;
//            first = second;
//            second = sum;
              second += first;
              first = second - first;
        }
        return second;
    }

    /**
     * 实现三:线性代数解法 – 特征方程
     * 时间复杂度:可视为O(1)
     */
    public static int fib3(int n) {
        double c = Math.sqrt(5);
        return (int) ((Math.pow((1 + c) / 2, n) - Math.pow((1 - c) / 2, n)) / c);
    }
}

1.2 度量算法优劣的方法

事后统计

这种方法可行但是有两个问题:

  • 一是要想对设计的算法的运行性能进行评测,需要实际运行该程序。
  • 二是所得时间的统计量依赖于计算机的硬件、软件等环境因素, 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快。

事前估计

通过分析某个算法的时间复杂度,空间复杂度来判断哪个算法更优。

2 时间复杂度

2.1 理解

一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度

T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的T(n) 不同,但时间复杂度相同,都为O(n²)。

2.2 大O表示法

一般用大O表示法来描述复杂度,它表示的是数据规模 n 对应的复杂度。如上述O( f(n) )

忽略常数、系数、低阶

  • 9 => O(1)

  • 2n + 3 => O(n)

  • n^2 + 2n + 6 => O(n^2 )

  • 4n^3 + 3n^2 + 22n + 100 => O(n^3 )

注意:大O表示法仅仅是一种粗略的分析模型,是一种估算,能帮助我们短时间内了解一个算法的执行效率

2.4 对数阶的细节

对数阶一般省略底数:log2(n) = log2(9) * log9(n)

所以 log2(n)、log9(n)统称为logn

2.5 计算时间复杂度的方法

  • 用常数1代替运行时间中的所有加法常数 T(n)=2n²+7n+6 => T(n)=2n²+7n+1

  • 修改后的运行次数函数中,只保留最高阶项 T(n)=2n²+7n+1 => T(n) = 2n²

  • 去除最高阶项的系数 T(n) = 2n² => T(n) = n² => O(n²)

2.6 常见的时间复杂度

  • 常数阶O(1)
  • 对数阶O(logn) //注意:底数不一定是2
  • 线性阶O(n)
  • 线性对数阶O(nlogn)
  • 平方阶O(n^2)
  • 立方阶O(n^3)
  • k次方阶O(n^k)
  • 指数阶O(2^n)
clipboard

说明

① 常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(logn)<Ο(n)<Ο(nlogn) <Ο(n2)<Ο(n3)< Ο(n^k) <Ο(2^n) <Ο(n^n),随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低

② 从图中可见,我们应该尽可能避免使用指数阶的算法

③ 对数阶一般忽略底数,所以log2n,log9n统称logn

2.7 时间复杂度练习

public class TimeComplexityTest {
    public static void test1(int n) {
        // 1
        if (n > 10) {
            System.out.println("n > 10");
        } else if (n > 5) { // 2
            System.out.println("n > 5");
        } else {
            System.out.println("n <= 5");
        }

        // 1 + 4 + 4 + 4
        for (int i = 0; i < 4; i++) {
            System.out.println("test");
        }

        // 14 => O(1)
    }

    public static void test2(int n) {
        // 1 + 3n => O(n)
        for (int i = 0; i < n; i++) {
            System.out.println("test");
        }
    }

    public static void test3(int n) {
        // 1 + 2n + n * (1 + 45)
        // => 48n + 1 => O(n)
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < 15; j++) {
                System.out.println("test");
            }
        }
    }

    public static void test4(int n) {
        // n = 8 = 2^3 ,可以执行3次
        // n = 16 = 2^4,可以执行4次
        // => n =  2^k,可以执行log2(n)次

        // log2(n) => O(logn)
        while ((n = n / 2) > 0) {
            System.out.println("test");
        }
    }

    public static void test5(int n) {
        // log5(n) => O(logn)
        while ((n = n / 5) > 0) {
            System.out.println("test");
        }
    }

    public static void test6(int n) {
        // i * 2^k = n
        // => k = log2(n/i) = log2(n)

        // 1 + log2(n) + log2(n)
        for (int i = 1; i < n; i = i * 2) {
            // 1 + 3n
            for (int j = 0; j < n; j++) {
                System.out.println("test");
            }
        }

        // 1 + 2*log2(n) + log2(n) * (1 + 3n)
        // => 1 + 3*log2(n) + 2 * nlog2(n)
        // => O(nlogn)
    }

    public static void test7(int n) {
        // 1 + 2n + n * (1 + 3n)
        // => 3n^2 + 3n + 1 => O(n^2)
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                System.out.println("test");
            }
        }
    }
    public static void test8(int n,int k) {
        //n
        for(int i = 0;i < n;i++) {
            System.out.println("test");
        }
        //k
        for(int i = 0;i < k;i++) {
            System.out.println("test");
        }
        //复杂度:O(n+k)
    }
}

2.8 平均时间复杂度和最坏时间复杂度

平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。

最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长

平均时间复杂度和最坏时间复杂度是否一致,和算法本身有关

2.9 均摊复杂度

什么情况下使用均摊复杂度:经过连续的多次复杂度比较低的情况后,出现个别复杂度比较高的情况。

案例:动态数组的扩容

2.10 复杂度震荡

什么是复杂度震荡:在一些特殊的情况下,某个级别的复杂度猛地蹿到了另一个级别,并且持续这一级别不恢复,则说明产生了复杂度震荡。

案例:动态数组扩容倍数、缩容时机设计不得当(扩容倍数*缩容倍数=1),有可能会导致复杂度震荡。

3. 空间复杂度

类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储间,它也是问题规模n的函数。

空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况.

在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间。

4. 算法的优化方向

  • 用尽量少的存储空间

  • 用尽量少的执行步骤(执行时间)

  • 根据情况,可以选择空间换时间,也可以时间换空间

三. 线性结构

1. 动态数组ArrayList

1.1 理解

数组是一种顺序存储的线性表,所有元素的内存地址是连续的。

image-20210801210207171

在很多编程语言中,数组都有个致命的缺点:无法动态修改容量。实际开发中,我们更希望数组的容量是可以动态改变的

1.1 属性设计

image-20210801211343289

1.2 接口设计

注意与ArrayList源码对比分析

public interface List<E> {
    /** 元素数量 */
    int size();

    /** 是否为空 */
    boolean isEmpty();

    /** 是否包含某个元素 */
    boolean contains(E element);

    /** 添加元素到末尾 */
    void add(E element);

    /** 获取index位置的元素 */
    E get(int index);

    /** 设置index位置的元素 */
    E set(int index,E element);

    /** 往index位置添加元素 */
    void add(int index,E element);

    /** 删除index位置对应的元素 */
    E remove(int index);

    /** 查看元素的位置 */
    int indexOf(E element);

    /** 清除所有元素 */
    void clear();
}

1.3 图解方法

添加元素-add(E element)

image-20210801211557567

添加元素-add(int index,E element)

image-20210801211843586

删除元素-remove(int index)

image-20210801211723778

如何扩容

image-20210801212040012

1.4 实现

public class ArrayList<E> implements List<E>{
    /** 元素数量 */
    private int size = 0;

    /** 所有元素 */
    private E[] elements;

    /**	默认容量 */
    private static final int DEFAULT_CAPACITY = 10;

    /** 元素未找到返回的下标 */
    private static final int ELEMENT_NOT_FOUND = -1;

    public ArrayList() {
        this(DEFAULT_CAPACITY);
    }

    public ArrayList(int capaticy) {
        capaticy = capaticy < DEFAULT_CAPACITY ? DEFAULT_CAPACITY : capaticy;
        elements = (E[])new Object[capaticy];
    }

    /**
     * @Description 判断下标是否越界
     */
    private void indexCheck(int index) {
        if(index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Index:" + index + ",Size:" + size);
        }
    }

    /**
     * @Description 数组容量不够则扩容
     */
    private void ensureCapacity(int size) {
        int oldCapacity = elements.length;
        if(size < oldCapacity) return;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍
        E[] newElements = (E[])new Object[newCapacity];
//        for (int i = 0; i < size; i++) {
//            newElements[i] = elements[i];
//        }
        System.arraycopy(elements,0,newElements,0,elements.length);
        elements = newElements;
        System.out.println("扩容:" + oldCapacity + "=>" + newCapacity);
    }

    /**
     * @Description 数组容量太多则缩容
     */
    private void trim() {
        int oldCapacity = elements.length;
        if(size >= (oldCapacity >> 1)) return;
        if(oldCapacity <= DEFAULT_CAPACITY) return;
        //剩余空间很多,可以缩容
        int newCapacity = oldCapacity >> 1;
        E[] newElements = (E[])new Object[newCapacity];
//        for (int i = 0; i < size; i++) {
//            newElements[i] = elements[i];
//        }
        System.arraycopy(elements,0,newElements,0,elements.length);
        elements = newElements;
        System.out.println("缩容:" + oldCapacity + "=>" + newCapacity);
    }

    /**
     * @Description 是否为空
     * @return
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * @Description 元素的数量
     * @return size
     */
    public int size() {
        return size;
    }

    /**
     * @Description 往index位置添加元素
     * @param index
     * @param element
     * @return
     */
    public void add(int index, E element) {
        //最好复杂度:O(1)、最坏复杂度:O(n)、平均复杂度:O(n)
        indexCheck(index);
        ensureCapacity(size);
        for(int i = size;i > index;i--) {
            elements[i] = elements[i - 1];
        }
        elements[index] = element;
        size++;
    }

    /**
     * @Description 添加元素到最后面
     * @param element
     */
    public void add(E element) {
        //最好:O(1)
        //最坏:O(n)  => 扩容的情况
        //平均:O(1)
        //均摊复杂度:O(1)  =>把扩容情况均摊到每一种情况去
        //         (一般均摊等于最好)。
        //什么情况下使用均摊复杂度:经过连续的多次复杂度比较低的
        //         情况后,出现个别复杂度比较高的情况。
        add(size, element);
    }

    /**
     * @Description 删除index位置对应的元素
     * @param index
     * @return oldEle
     */
    public E remove(int index) {
        //最好复杂度:O(1)、最坏复杂度:O(n)、平均复杂度:O(n)
        indexCheck(index);
        E oldEle = elements[index];
        if(index != size - 1) {
            for(int i = index;i < size;i++) {
                elements[i] = elements[i + 1];
            }
        }
        elements[--size] = null;//内存管理细节

        trim(); //内存紧张考虑缩容
        return oldEle;
    }

    /**
     * @Description 删除某个元素
     * @param element
     */
    public void remove(E element) { //O(1)
        remove(indexOf(element));
    }

    /**
     * @Description 设置index位置的元素
     * @param index
     * @param element
     * @return
     */
    public E set(int index, E element) {  //O(1)
        indexCheck(index);
        E old = elements[index];
        elements[index] = element;
        return old;
    }

    /**
     * @Description 返回index位置对应的元素
     * @param index
     * @return
     */
    public E get(int index) {
        indexCheck(index);
        return elements[index];
    }

    /**
     * @Description 查看元素的位置
     * @param element
     * @return
     */
    public int indexOf(E element) {
        if(element == null) {
            for (int i = 0; i < size; i++) {
                if(elements[i] == null) return i;
            }
        } else {
            for (int i = 0; i < size; i++) {
                if(element.equals(elements[i])) return i;
            }
        }
        return ELEMENT_NOT_FOUND;
    }

    /**
     * @Description 是否包含某个元素
     * @param element
     * @return
     */
    public boolean contains(E element) {
        return indexOf(element) != ELEMENT_NOT_FOUND;
    }

    /**
     * @Description 清除所有元素
     */
    public void clear() {
        //方法一:只是访问不到了,数组每一个位置对应的对象还存在。
        //       当对某个位置再次add操作时,此位置存储的地址值对
        //       应以前的对象才会被销毁。
        //size = 0;
        //方法二:对每一个位置对应的对象地址值置空(内存管理细节)
        for (int i = 0; i < size; i++) {
            elements[i] = null;
        }
        size = 0;
    }

    @Override
    public String toString() {
        StringBuilder str = new StringBuilder();
        str.append("size=").append(size).append(" : [");
        for (int i = 0; i < size; i++) {
            if(i != 0) str.append(", ");
            str.append(elements[i]);
        }
        str.append("]");
        return str.toString();
    }
}

2. 单向链表LinkedList

2.1 理解

动态数组有个明显的缺点:可能会造成内存空间的大量浪费。能否用到多少就申请多少内存?链表可以办到这一点。

链表存储结构的特点:

  • 链表是一种链式存储的线性表,通过指针域描述数据元素之间的逻辑关系,不需要地址连续的存储空间。
  • 动态存储空间分配,即时申请即时使用。
  • 访问第i个元素,必须顺序依此访问前面的1 ~ i-1的数据元素,也就是说是一种顺序存取结构。
    插入/删除操作不需要移动数据元素。

注意:

① Java中如何实现“指针”Java中的对象引用变量并不是存储实际数据,而是存储该对象在内存中的存储地址。

② 链表分为带头节点的链表和没有头节点的链表,根据实际的需求来确定。

2.2 图解方法

clipboard

2.3 实现

class SingleLinkedList<E> implements List<E>{
	/**
	 *	元素的数量
	 */
	private int size;
	
	/**
	 *  指向第一个节点的指针
	 */
	private Node<E> first;

	/**
	 * 	元素未找到返回的下标
	 */
	private static final int ELEMENT_NOT_FOUND = -1;

	/**
	 * @Description 判断下标是否越界
	 */
	private void indexCheck(int index) {
		if(index < 0 || index >= size) {
			throw new IndexOutOfBoundsException("Index:" 
					+ index + ",Size:" + size);
		}
	}
	
	/**
	 * @Description 获取index位置对应的节点
	 * @return
	 */
	private Node<E> getNode(int index) {
		indexCheck(index);
		Node<E> temp = first;
		for(int i = 0;i < index;i++) {
			temp = temp.next;
		}
		return temp;
	}

	/**
	 * @Description 是否为空
	 * @return
	 */
	public boolean isEmpty() {
		return size == 0;
	}

	/**
	 * @Description 元素的数量
	 * @return size
	 */
	public int size() {
		return size;
	}

	/**
	 * @Description 往index位置添加元素
	 * @param index
	 * @param element
	 * @return
	 */
	public void add(int index, E element) {
		if(index < 0 || index > size) {
			throw new IndexOutOfBoundsException("Index:" 
					+ index + ",Size:" + size);
		}
		if (index == 0) {
			first = new Node<>(element, first);
		} else {
			Node<E> prev = getNode(index - 1);
			prev.next = new Node<>(element, prev.next);
		}
		size++;
	}

	/**
	 * @Description 添加元素到最后面
	 * @param element
	 */
	public void add(E element) {
		add(size, element);
	}

	/**
	 * @Description 删除index位置对应的元素
	 * @param index
	 * @return oldEle
	 */
	public E remove(int index) {
		indexCheck(index);
		Node<E> node = first;
		if (index == 0) {
			first = first.next;
		} else {
			Node<E> prev = getNode(index - 1);
			node = prev.next;
			prev.next = node.next;
		}
		size--;
		return node.element;
	}

	/**
	 * @Description 设置index位置的元素
	 * @param index
	 * @param element
	 * @return
	 */
	public E set(int index, E element) {
		Node<E> node = getNode(index);
		E oldElement = node.element;
		node.element = element;
		return oldElement;
	}

	/**
	 * @Description 返回index位置对应的元素
	 * @param index
	 * @return
	 */
	public E get(int index) {
		return getNode(index).element;
	}

	/**
	 * @Description 查看元素的位置
	 * @param element
	 * @return
	 */
	public int indexOf(E element) {
		if (element == null) {
			Node<E> node = first;
			for (int i = 0; i < size; i++) {
				if (node.element == null) return i;
				
				node = node.next;
			}
		} else {
			Node<E> node = first;
			for (int i = 0; i < size; i++) {
				if (element.equals(node.element)) return i;
				
				node = node.next;
			}
		}
		return ELEMENT_NOT_FOUND;
	}

	/**
	 * @Description 是否包含某个元素
	 * @param element
	 * @return
	 */
	public boolean contains(E element) {
		return indexOf(element) != ELEMENT_NOT_FOUND;
	}

	/**
	 * @Description 清除所有元素
	 */
	public void clear() {
		size = 0;
		first = null;
	}
	
	@Override
	public String toString() {
		Node<E> temp = first;
		StringBuilder str = new StringBuilder();
		for(int i = 0;i < size;i++) {
			if(i != 0) {
				str.append(",");
			}
			str.append(temp.element);
			temp = temp.next;
		}
		return "size=" + size + ", [" + str + "]";
	}
	
	/**
	 * @Description 节点内部类
	 */
	private static class Node<E> {
		E element;
		Node<E> next;
		
		public Node(E element, Node<E> next) {
			this.element = element;
			this.next = next;
		}

		@Override
		public String toString() {
			return element + "";
		}
	}
	
}

3. 双向链表LinkedList

3.1 单向链表缺点

单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。

单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除节点,总是要先找到待删除节点的前一个节点。

3.2 实现

public class LinkedList<E> implements List<E>{
	/**
	 *	元素的数量
	 */
	private int size;
	
	/**
	 *  指向第一个节点的指针
	 */
	private Node<E> first;

	/**
	 *  指向最后一个节点的指针
	 */
	private Node<E> last;
	
	/**
	 * 	元素未找到返回的下标
	 */
	private static final int ELEMENT_NOT_FOUND = -1;

	/**
	 * @Description 判断下标是否越界
	 */
	private void indexCheck(int index) {
		if(index < 0 || index >= size) {
			throw new IndexOutOfBoundsException("Index:" 
					+ index + ",Size:" + size);
		}
	}
	
	/**
	 * @Description 获取index位置对应的节点
	 * @return
	 */
	private Node<E> getNode(int index) {
		indexCheck(index);
		if(index < (size << 1)) {
			Node<E> temp = first;
			for(int i = 0;i < index;i++) {
				temp = temp.next;
			}
			return temp;
		} else {
			Node<E> temp = last;
			for(int i = size - 1;i > index;i--) {
				temp = temp.prev;
			}
			return temp;
		}
	}

	/**
	 * @Description 是否为空
	 * @return
	 */
	public boolean isEmpty() {
		return size == 0;
	}

	/**
	 * @Description 元素的数量
	 * @return size
	 */
	public int size() {
		return size;
	}

	/**
	 * @Description 往index位置添加元素
	 * @param index
	 * @param element
	 * @return
	 */
	public void add(int index, E element) {
		if(index < 0 || index > size) {
			throw new IndexOutOfBoundsException("Index:" 
					+ index + ",Size:" + size);
		}
		if(index == size) { //往最后面添加元素时
			Node<E> oldLast = last;
			last = new Node<E>(oldLast,element,null);
			if(oldLast == null) { //链表添加第一个元素时
				first = last;
			} else {
				oldLast.next = last;
			}
		} else {
			Node<E> next = getNode(index);
			Node<E> prev = next.prev;
			Node<E> node = new Node<E>(prev,element,next);
			next.prev = node;
			if(prev == null) { //=>index == 0时
				first = node;
			} else {
				prev.next = node;
			}
		}
		
		size++;
	}

	/**
	 * @Description 添加元素到最后面
	 * @param element
	 */
	public void add(E element) {
		add(size, element);
	}

	/**
	 * @Description 删除index位置对应的元素
	 * @param index
	 * @return oldEle
	 */
	public E remove(int index) {
		indexCheck(index);
		Node<E> node = getNode(index);
		Node<E> prev = node.prev;
		Node<E> next = node.next;
		if(prev == null) { //index == 0
			first = next;
		} else {
			prev.next = next;
		}
		if(next == null) { //index == size - 1
			last = prev;
		} else {
			next.prev = prev;
		}
		size--;
		return node.element;
	}

	/**
	 * @Description 设置index位置的元素
	 * @param index
	 * @param element
	 * @return
	 */
	public E set(int index, E element) {
		Node<E> node = getNode(index);
		E oldElement = node.element;
		node.element = element;
		return oldElement;
	}

	/**
	 * @Description 返回index位置对应的元素
	 * @param index
	 * @return
	 */
	public E get(int index) {
		return getNode(index).element;
	}

	/**
	 * @Description 查看元素的位置
	 * @param element
	 * @return
	 */
	public int indexOf(E element) {
		if (element == null) {
			Node<E> node = first;
			for (int i = 0; i < size; i++) {
				if (node.element == null) return i;
				
				node = node.next;
			}
		} else {
			Node<E> node = first;
			for (int i = 0; i < size; i++) {
				if (element.equals(node.element)) return i;
				
				node = node.next;
			}
		}
		return ELEMENT_NOT_FOUND;
	}

	/**
	 * @Description 是否包含某个元素
	 * @param element
	 * @return
	 */
	public boolean contains(E element) {
		return indexOf(element) != ELEMENT_NOT_FOUND;
	}

	/**
	 * @Description 清除所有元素
	 */
	public void clear() {
		size = 0;
		first = null;
		last = null;
		/*
		 * gc root对象:① 被栈指针指向的对象,如new LinkedList()
		 * 
		 * => 只要断掉first和last,当前链表不被gc root对象指向就
		 *    会被回收。
		 */
	}
	
	@Override
	public String toString() {
		Node<E> temp = first;
		StringBuilder str = new StringBuilder();
		for(int i = 0;i < size;i++) {
			if(i != 0) {
				str.append(",");
			}
			str.append(temp);
			temp = temp.next;
		}
		return "size=" + size + ", [" + str + "]";
	}
	
	/**
	 * @Description 节点内部类
	 */
	private static class Node<E> {
		Node<E> prev;
		E element;
		Node<E> next;
		
		public Node(Node<E> prev,E element, Node<E> next) {
			this.prev = prev;
			this.element = element;
			this.next = next;
		}

		@Override
		public String toString() {
			StringBuilder str = new StringBuilder();
			if(prev != null) {
				str.append(prev.element);
			}
			str.append("_").append(element).append("_");
			if(next != null) {
				str.append(next.element);
			}
			return str + "";
		}
	}
	
}

3.3 ArrayList与LinkedList对比

ArrayList开辟,销毁内存空间的次数相对较少,但可能造成内存空间浪费(缩容解决)。LinkedList开辟、销毁内存空间的次数相对较多,但不会造成内存空间的浪费。

如果频繁在尾部进行添加,删除操作,动态数组与双向链表均可选择。

如果频繁在头部进行添加,删除操作,建议选择使用双向链表。

如果有频繁的(在任意位置)添加,删除操作,建议选择双向链表。

如果有频繁的查询操作(随机访问操作),建议选择动态数组。

是否有了双向链表,单向链表就没任何用处了? => 并非如此,在哈希表的设计中就用到了单链表。

4. 循环链表LinkedList

4.1 单向循环链表

注意:单向循环链表相对于单链表(SingleLinkedList)只需修改添加和删除。

/**
 * @Description 往index位置添加元素
 * @param index
 * @param element
 * @return
 */
public void add(int index, E element) {
	if(index < 0 || index > size) {
		throw new IndexOutOfBoundsException("Index:" 
				+ index + ",Size:" + size);
	}
	if (index == 0) {
		Node<E> newFirst = new Node<>(element, first);
		//拿到最后一个节点
		Node<E> last = (size == 0) ? newFirst : getNode(size - 1);
		last.next = newFirst;
		first = newFirst;
	} else {
		Node<E> prev = getNode(index - 1);
		prev.next = new Node<>(element, prev.next);
	}
	size++;
}
/**
 * @Description 删除index位置对应的元素
 * @param index
 * @return oldEle
 */
public E remove(int index) {
	indexCheck(index);
	Node<E> node = first;
	if (index == 0) {
		if(size == 1) {
			first = null;
		} else {
			//拿到最后一个节点,注意一定要在改变first之前
			Node<E> last = getNode(size - 1);
			first = first.next;
			last.next = first;
		}
	} else {
		Node<E> prev = getNode(index - 1);
		node = prev.next;
		prev.next = node.next;
	}
	size--;
	return node.element;
}

4.2 双向循环链表

注意:双向循环链表相对于双向链表(LinkedList)只用修改添加和删除。

clipboard (1)
/**
 * @Description 往index位置添加元素
 * @param index
 * @param element
 * @return
 */
public void add(int index, E element) {
	if(index < 0 || index > size) {
		throw new IndexOutOfBoundsException("Index:" 
				+ index + ",Size:" + size);
	}
	if(index == size) { //往最后面添加元素时
		Node<E> oldLast = last;
		last = new Node<E>(oldLast,element,first);
		if(oldLast == null) { //链表添加第一个元素时
			first = last;
			first.next = first;
			first.prev = first;
		} else {
			oldLast.next = last;
			first.prev = last;
		}
	} else {
		Node<E> next = getNode(index);
		Node<E> prev = next.prev;
		Node<E> node = new Node<E>(prev,element,next);
		next.prev = node;
		prev.next = node;
		if(index == 0) { //=>index == 0时
			first = node;
		}
	}
	size++;
}

/**
 * @Description 删除index位置对应的元素
 * @param index
 * @return node.element
 */
public E remove(int index) {
	indexCheck(index);
	Node<E> node = first;
	if(size == 1) {
		first = null;
		last = null;
	} else {
		node = getNode(index);
		Node<E> prev = node.prev;
		Node<E> next = node.next;
		prev.next = next;
		next.prev = prev;
		if(index == 0) { //index == 0
			first = next;
		} 
		if(index == size - 1) { //index == size - 1
			last = prev;
		} 
	}
	size--;
	return node.element;
}

4.3 约瑟夫问题 (单向循环链表的应用)

约瑟夫问题:设编号为1,2,3...n的n个人围成一圈,约定编号为 k (1 <= k <= n)的人从1开始报数,数到m的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依此类推,直到所有人出列为止,由此产生一个出列编号的序列。

注意:约瑟夫问题也可以用其它数据结构解决,不一定要用循环链表,但是循环链表解决此问题很简单。

clipboard (2)

使用循环链表解决约瑟夫问题

为了发挥循环链表的最大威力,可对CircleLinkedList做如下改进:

clipboard (3)
public class LinkedListTest {	
	@Test
	public void test1() {
		CircleLinkedListForJosephus<Integer> list 
			= new CircleLinkedListForJosephus<Integer>();
		for(int i = 1; i <= 8;i++) {
			list.add(i);
		}
		//current指向头节点
		list.reset();
		while(!list.isEmpty()) {
			list.next();
			list.next();
			System.out.print(list.remove() + " ");//数了三次后删除
			//3 6 1 5 2 8 4 7 
		}
	}
}

class CircleLinkedListForJosephus<E> implements List<E>{
	/**
	 *	元素的数量
	 */
	private int size;
	
	/**
	 *  指向第一个节点的指针
	 */
	private Node<E> first;

	/**
	 *  指向最后一个节点的指针
	 */
	private Node<E> last;
	
	/**
	 * 用于指向某个节点的指针
	 */
	private Node<E> current;
	
	/**
	 * 	元素未找到返回的下标
	 */
	private static final int ELEMENT_NOT_FOUND = -1;

	/**
	 * @Description 判断下标是否越界
	 */
	private void indexCheck(int index) {
		if(index < 0 || index >= size) {
			throw new IndexOutOfBoundsException("Index:" 
					+ index + ",Size:" + size);
		}
	}
	
	/**
	 * @Description 获取index位置对应的节点
	 * @return
	 */
	private Node<E> getNode(int index) {
		indexCheck(index);
		if(index < (size << 1)) {
			Node<E> temp = first;
			for(int i = 0;i < index;i++) {
				temp = temp.next;
			}
			return temp;
		} else {
			Node<E> temp = last;
			for(int i = size - 1;i > index;i--) {
				temp = temp.prev;
			}
			return temp;
		}
	}

	/**
	 * @Description 让current指向头节点
	 */
	public void reset() {
		current = first;
	}
	
	/**
	 * @Description 让current后移一步
	 * @return
	 */
	public E next() {
		if(current == null) return null;
		current = current.next;
		return current.element;
	}
	
	/**
	 * @Description 删除current所指向的节点,并将current下移
	 * @return
	 */
	public E remove() {
		if(current == null) return null;
		Node<E> next = current.next;
		int index = indexOf(current.element);
		E element = remove(index);
		if(size == 0) {
			current = null;
		} else {
			current = next;
		}
		return element;
	}
	
	/**
	 * @Description 是否为空
	 * @return
	 */
	public boolean isEmpty() {
		return size == 0;
	}

	/**
	 * @Description 元素的数量
	 * @return size
	 */
	public int size() {
		return size;
	}

	/**
	 * @Description 往index位置添加元素
	 * @param index
	 * @param element
	 * @return
	 */
	public void add(int index, E element) {
		if(index < 0 || index > size) {
			throw new IndexOutOfBoundsException("Index:" 
					+ index + ",Size:" + size);
		}
		if(index == size) { //往最后面添加元素时
			Node<E> oldLast = last;
			last = new Node<E>(oldLast,element,first);
			if(oldLast == null) { //链表添加第一个元素时
				first = last;
				first.next = first;
				first.prev = first;
			} else {
				oldLast.next = last;
				first.prev = last;
			}
		} else {
			Node<E> next = getNode(index);
			Node<E> prev = next.prev;
			Node<E> node = new Node<E>(prev,element,next);
			next.prev = node;
			prev.next = node;
			if(index == 0) { //=>index == 0时
				first = node;
			}
		}
		size++;
	}

	/**
	 * @Description 添加元素到最后面
	 * @param element
	 */
	public void add(E element) {
		add(size, element);
	}

	/**
	 * @Description 删除index位置对应的元素
	 * @param index
	 * @return node.element
	 */
	public E remove(int index) {
		indexCheck(index);
		Node<E> node = first;
		if(size == 1) {
			first = null;
			last = null;
		} else {
			node = getNode(index);
			Node<E> prev = node.prev;
			Node<E> next = node.next;
			prev.next = next;
			next.prev = prev;
			if(index == 0) { //index == 0
				first = next;
			} 
			if(index == size - 1) { //index == size - 1
				last = prev;
			} 
		}
		size--;
		return node.element;
	}

	/**
	 * @Description 设置index位置的元素
	 * @param index
	 * @param element
	 * @return
	 */
	public E set(int index, E element) {
		Node<E> node = getNode(index);
		E oldElement = node.element;
		node.element = element;
		return oldElement;
	}

	/**
	 * @Description 返回index位置对应的元素
	 * @param index
	 * @return
	 */
	public E get(int index) {
		return getNode(index).element;
	}

	/**
	 * @Description 查看元素的位置
	 * @param element
	 * @return
	 */
	public int indexOf(E element) {
		if (element == null) {
			Node<E> node = first;
			for (int i = 0; i < size; i++) {
				if (node.element == null) return i;
				
				node = node.next;
			}
		} else {
			Node<E> node = first;
			for (int i = 0; i < size; i++) {
				if (element.equals(node.element)) return i;
				
				node = node.next;
			}
		}
		return ELEMENT_NOT_FOUND;
	}

	/**
	 * @Description 是否包含某个元素
	 * @param element
	 * @return
	 */
	public boolean contains(E element) {
		return indexOf(element) != ELEMENT_NOT_FOUND;
	}

	/**
	 * @Description 清除所有元素
	 */
	public void clear() {
		size = 0;
		first = null;
		last = null;
		/*
		 * gc root对象:① 被栈指针指向的对象,如new LinkedList()
		 * 
		 * => 只要断掉first和last,当前链表不被gc root对象指向就
		 *    会被回收。
		 */
	}
	
	@Override
	public String toString() {
		Node<E> temp = first;
		StringBuilder str = new StringBuilder();
		for(int i = 0;i < size;i++) {
			if(i != 0) {
				str.append(",");
			}
			str.append(temp);
			temp = temp.next;
		}
		return "size=" + size + ", [" + str + "]";
	}
	
	/**
	 * @Description 节点内部类
	 */
	private static class Node<E> {
		Node<E> prev;
		E element;
		Node<E> next;
		
		public Node(Node<E> prev,E element, Node<E> next) {
			this.prev = prev;
			this.element = element;
			this.next = next;
		}

		@Override
		public String toString() {
			StringBuilder str = new StringBuilder();
			if(prev != null) {
				str.append(prev.element);
			}
			str.append("_").append(element).append("_");
			if(next != null) {
				str.append(next.element);
			}
			return str + "";
		}
	}
	
}

5. 栈(stack)

5.1 理解

栈是一个先入后出(FILO => First In Last Out)的有序列表。往栈中添加元素的操作,一般叫做入栈(push)。从栈中移除元素的操作,一般叫做 出栈(pop),注意只能移除栈顶元素,也叫做弹出栈顶元素。

栈是限制线性表中元素的插入和删除 只能在线性表的同一端 进行的一种特殊线性表。允许插入和删除的一端为变化的一端,称为 栈顶(Top),另一端为固定的一端,称为 栈底(Bottom)

出栈(pop)和入栈(push)的概念如下:

clipboard (4)

注意:这里说的“栈”与内存中的“栈空间”是两个不同的概念。

5.2 栈的应用场景

  • 子程序的调用:在跳往子程序前,会先将下一个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。

  • 处理递归调用:和子程序的调用类似,只是除了存储下一个指令的地址外,也将参数,区域变量等数据存入堆栈中。

  • 表达式的转换与求值(实际解决):如中缀表达式转后缀表达式

  • 二叉树的遍历

  • 图形的深度优先(depth-first)搜索法

5.3 ArrayList模拟栈

public class StackTest {
	@Test
	public void test1() {
		ArrayListStack<Integer> stack = new ArrayListStack<Integer>();
		stack.push(11);
		stack.push(22);
		stack.push(33);
		stack.push(44);
		System.out.println(stack.peek());//44
           stack.list();
		while(!stack.isEmpty()) {
			System.out.print(stack.pop() + " ");
			//44 33 22 11 
		}
		System.out.println(stack.isEmpty());
		
	}
}

class ArrayListStack<E> {
	private List<E> list = new ArrayList<E>();
	
	//栈的长度
	public int size() {
		return list.size();
	}
	
	// 判断栈空
	public boolean isEmpty() {
		return list.isEmpty();
	}

	// 入栈
	public void push(E element) {
		list.add(element);
	}

	// 出栈
	public E pop() {
		return list.remove(list.size() - 1);
	}

	// 获取栈顶元素
	public E peek() {
		return list.get(list.size() - 1);
	}
 
   //遍历栈
	public void list() {
		if(isEmpty()) {
			System.out.println("栈空,无数据!");
			return;
		}
		for (int i = list.size() - 1; i >= 0; i--) {
			System.out.println("stack[" + i + "] = " + list.get(i));
		}
	}
}

5.4 LinkedList模拟栈

//只需要将以上代码的
private List<E> list = new ArrayList<E>();
//改为
private List<E> list = new LinkedList<E>();

5.5 栈的应用-综合计算器(自定义优先级)

即使用栈计算一个中缀表达式的结果

clipboard (5)
public class Calculator {
	public static void main(String[] args) {
		String expression = "7*2*2-5+1+4/2";
		calculator(expression);
            //表达式 7*2*2-5+1+4/2 的结果为:26
	}
	
	public static void calculator(String expression) {
		ArrayListStack2<Integer> numStack = new ArrayListStack2<>();
		ArrayListStack2<Integer> operStack = new ArrayListStack2<>();
		int index = 0;//用于扫描
		int num1 = 0;
		int num2 = 0;
		int oper = 0;
		int res = 0;
		char ch = ' ';//将每次扫描得到的字符保存到ch
		String keepNum = "";//用于拼接多位数
		//开始循环扫描expression
		while(true) {
			ch = expression.substring(index, index + 1).charAt(0);
			if(operStack.isOper(ch)) {
				if(!operStack.isEmpty()) {
					if(operStack.priority(ch) <= 
							operStack.priority(operStack.peek())) {
						num1 = numStack.pop();
						num2 = numStack.pop();
						oper = operStack.pop();
						res = numStack.cal(num1, num2, oper);
						//将运算结果入数栈
						numStack.push(res);
						//将操作符入符号栈
						operStack.push(ch + 0);
					} else {
						//当前操作符的优先级大于栈中的操作符优先级,直接入符号栈
						operStack.push(ch + 0);
					}
				} else {
					//符号栈为空就直接入符号栈
					operStack.push(ch + 0);
				}
			} else {
				//如果是数就直接入数栈
				//numStack.push(ch - 48);//'1' => 1  (只能处理一位数)
				//能够处理多位数的思路:
					//当处理数时,需要向expression表达式的index后再看一位,
					//如果是数就拼接并继续扫描,是符号才入栈
				keepNum += ch;
				if(index == expression.length() - 1) {
					//如果ch已经是expression的最后一位,就直接入栈
					numStack.push(Integer.parseInt(keepNum));
				} else {
					if(operStack.isOper(expression.substring(index + 1, index + 2)
							.charAt(0))) {
						numStack.push(Integer.parseInt(keepNum));
						//将keepNum清空
						keepNum = "";
					}
				}
			}
			//使index + 1,并判断是否扫描到expression的最后
			index++;
			if(index >= expression.length()) {
				break;
			}
		}
		
		//当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号并运算
		while(true) {
			if(operStack.isEmpty()) {
				break;
			}
			num1 = numStack.pop();
			num2 = numStack.pop();
			oper = operStack.pop();
			res = numStack.cal(num1, num2, oper);
			numStack.push(res);
		}
		//将数栈的最后数pop出,得到结果
		int res2 = numStack.pop();
		System.out.println("表达式 " + expression 
				+ " 的结果为:" + res2);
	}
}

//数组模拟栈,需要扩展一些功能
class ArrayListStack2<E> {
	private List<E> list = new ArrayList<E>();
	
	//栈的长度
	public int size() {
		return list.size();
	}
	
	// 判断栈空
	public boolean isEmpty() {
		return list.isEmpty();
	}

	// 入栈
	public void push(E element) {
		list.add(element);
	}

	// 出栈
	public E pop() {
		return list.remove(list.size() - 1);
	}

	// 获取栈顶元素
	public E peek() {
		return list.get(list.size() - 1);
	}
	
	//遍历栈
	public void list() {
		if(isEmpty()) {
			System.out.println("栈空,无数据!");
			return;
		}
		for (int i = list.size() - 1; i >= 0; i--) {
			System.out.println("stack[" + i + "] = " + list.get(i));
		}
	}
	
	//返回运算符的自定义优先级(假定优先级使用数字表示)
	public int priority(int oper) {
		if(oper == '*' || oper == '/') {
			return 1;
		} else if(oper == '+' || oper == '-') {
			return 0;
		} else {
			return -1;//假定目前的表达式只有+-*/
		}
	}
	
	//判断当前字符是否是一个运算符
	public boolean isOper(char val) {
		return val == '+' || val == '-' || val == '*' || val == '/';
	}
	
	//计算两个操作数的方法
	public int cal(int num1,int num2,int oper) {
		int res = 0;
		switch(oper) {
		case '+':
			res = num2 + num1;
			break;
		case '-':
			res = num2 - num1;
			break;
		case '*':
			res = num2 * num1;
			break;
		case '/':
			res = num2 / num1;
			break;
		default:
			break;
		}
		return res;
	}
}

5.6 栈的应用-逆波兰计算器

逆波兰表达式(前缀表达式)

前缀表达式的运算符位于操作数之前。

前缀表达式的计算机求值:从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果

//举例:(3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤如下:
//① 从右至左扫描,将6、5、4、3压入堆栈
//② 遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈
//③ 接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈
//④ 最后是-运算符,计算出35-6的值,即29,由此得出最终结果

中缀表达式

中缀表达式就是常见的运算表达式,如(3+4)×5-6

中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(上述案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式)

后缀表达式

与前缀表达式相似,只是运算符位于操作数之后

clipboard (6)

后缀表达式的计算机求值

从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果

//例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下:
//① 从左至右扫描,将3和4压入堆栈;
//② 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
//③ 将5入栈;
//④ 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
//⑤ 将6入栈;
//⑥ 最后是-运算符,计算出35-6的值,即29,由此得出最终结果

中缀表达式转换为后缀表达式

后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,需要将中缀表达式转成后缀表达式

具体步骤:

//1.初始化两个栈:运算符栈s1和储存中间结果的栈s2;
//2.从左至右扫描中缀表达式;
//3.遇到操作数时,将其压s2;
//4.遇到运算符时,比较其与s1栈顶运算符的优先级:
//    ① 如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
//    ② 否则,若优先级比栈顶运算符的高,也将运算符压入s1;
//    ③ 否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶
//      运算符相比较; 
//5.遇到括号时:
//   ① 如果是左括号“(”,则直接压入s1
//   ② 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号
//     为止,此时将这一对括号丢弃
//6.重复步骤2至5,直到表达式的最右边
//7.将s1中剩余的运算符依次弹出并压入s2
//8.依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
clipboard (7) clipboard (8)

使用栈实现逆波兰计算器(计算整数)

public class ReversePolishCalculate {
	//直接输入一个后缀表达式计算结果
	@Test
	public void test1() {
		// 定义一个逆波兰表达式
		// 为了方便,逆波兰表达式的数字和符号使用空格隔开
		// (30+4)*5-6 => 30 4 + 5 * 6 - => 29
		// 4*5-8+60+8/2 => 4 5 * 8 - 60 + 8 2 / + => 76
		String suffixExpression = "4 5 * 8 - 60 + 8 2 / +";
		// 思路:
		//   ① 先将 "3 4 + 5 * 6 -" 放入ArrayList中
		//   ② 将ArrayList 传递给一个方法,遍历ArrayList配合栈完成计算

		List<String> list = getListString(suffixExpression);
		System.out.println(list);
		int res = calculate(list);
		System.out.println("计算的结果是 " + res);
	}
	
	// 完成将一个中缀表达式转为后缀表达式的功能并计算结果
	@Test
	public void test2() {
		// 思路:
		// 	 ① 直接对str操作不方便,因此先将中缀表达式字符串转换为List
		//   ② 中缀表达式对应的list => 后缀表达式对应的list
		String expression = "1+((2+3)*4)-5";
		List<String> list = toInfixExpressionList(expression);
		System.out.println(list);
		// [1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]
		List<String> list2 = parseSuffixExpressionList(list);
		System.out.println(list2);
		//[1, 2, 3, +, 4, *, +, 5, -]
		System.out.println("expression的计算结果为:" + calculate(list2));//16
	}

	// 将一个中缀表达式转换成对应的List
	public static List<String> toInfixExpressionList(String s) {
		List<String> ls = new ArrayList<String>();
		int i = 0;// 用于遍历中缀表达式字符串s的指针
		String str;// 对多位数的拼接
		char c;// 每遍历一个字符,就放入c
		do {
			// '0' => [48] '9' => [57]
			if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) {
				// 如果c是一个非数字,就加入ls中
				ls.add("" + c);
				i++;// i需要后移
			} else {
				// 如果是一个数字,需要考虑多位数
				str = "";// 先将str置空
				while (i < s.length() && (c = s.charAt(i)) > 48 && (c = s.charAt(i)) <= 57) {
					str += c;// 拼接
					i++;
				}
				ls.add(str);
			}
		} while (i < s.length());
		return ls;
	}

	//中缀表达式对应的list => 后缀表达式对应的list
	public static List<String> parseSuffixExpressionList(List<String> ls) {
		Stack<String> s1 = new Stack<String>();// 符号栈
		// 注意:因为s2这个栈在整个转换过程中没有pop操作且后面要逆序输出,
		//      很麻烦,所以用ArrayList代替。
		// Stack<String> s2 = new Stack<String>();//存储中间结果的栈
		List<String> s2 = new ArrayList<String>();// 存储中间结果的List
		// 遍历ls
		for (String item : ls) {
			// 如果是一个数字,入s2
			if (item.matches("\\d+")) {
				s2.add(item);
			} else if (item.equals("(")) {
				s1.push(item);
			} else if(item.equals(")")) {
				while(!s1.peek().equals("(")) {
					s2.add(s1.pop());
				}
				s1.pop();//将"("弹出s1栈
			} else {
				//思路:当item的优先级小于等于s1栈顶运算符时,将s1栈顶的运算符弹出
				//      并加入s2中,再次与s1中新的栈顶运算符比较优先级。
				//注意:我们需要一个比较运算符优先级高低的方法
				while(s1.size() != 0 && 
						Operation.getValue(s1.peek()) 
						>= Operation.getValue(item)) {
					s2.add(s1.pop());
				}
				//还需要将item压入栈中
				s1.push(item);
			}
		}
		//将s1中剩余的运算符依次弹出并加入s2
		while(s1.size() != 0) {
			s2.add(s1.pop());
		}
		return s2;//注意因为是存放到List中的,所以按顺序输出即可
	}

	// 将一个逆波兰表达式的数据和运算符放入ArrayList中
	public static List<String> getListString(String suffixExpression) {
		// 将suffixExpression分隔
		String[] split = suffixExpression.split(" ");
		ArrayList<String> list = new ArrayList<String>();
		for (String ele : split) {
			list.add(ele);
		}
		return list;
	}

	// 完成对逆波兰表达式的运算
	public static int calculate(List<String> ls) {
		Stack<String> stack = new Stack<String>();
		for (String item : ls) {
			// 使用正则表达式取出数
			if (item.matches("\\d+")) {// 匹配的是多位数
				stack.push(item);
			} else {
				// pop出两个数运算,结果入栈
				int num2 = Integer.parseInt(stack.pop());
				int num1 = Integer.parseInt(stack.pop());
				int res = 0;
				if (item.equals("+")) {
					res = num1 + num2;
				} else if (item.equals("-")) {
					res = num1 - num2;
				} else if (item.equals("*")) {
					res = num1 * num2;
				} else if (item.equals("/")) {
					res = num1 / num2;
				} else {
					throw new RuntimeException("运算符有误!");
				}
				// 把res入栈
				stack.push("" + res);
			}
		}
		// 最后留在stack中数据就是运算结果
		return Integer.parseInt(stack.pop());
	}
}

//返回一个运算符对应的优先级的类
class Operation {
	private static int ADD = 1;
	private static int SUB = 1;
	private static int MUL = 2;
	private static int DIV = 2;
	
	public static int getValue(String operation) {
		int result = 0;
		switch (operation) {
		case "+":
			result = ADD;
			break;
		case "-":
			result = SUB;
			break;
		case "*":
			result = MUL;
			break;
		case "/":
			result = DIV;
			break;
		default:
			System.out.println("不存在该运算符!");
			break;
		}
		return result;
	}
}

6.队列(queue)

6.1 理解

  • 队列是一个有序列表,可以用数组链表来实现。

  • 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出

  • 队尾(rear):只能从队尾添加元素,一般叫做 入队(enQueue)

  • 队头(front):只能从队头移除元素,一般叫做 出队(deQueue)

clipboard (9)

注意:队列优先使用双向链表实现,因为队列主要是往头尾操作元素。

6.2 LinkedList(双向链表)模拟队列

Java官方使用LinkedList实现了Queue接口

public class LinkedListQueue<E> {
	private List<E> list = new LinkedList<E>();

	//元素的数量
	public int size() {
		return list.size();
	}
	// 判断队列是否为空
	public boolean isEmpty() {
		return list.isEmpty();
	}

	// 入队
	public void enQueue(E element) {
		list.add(element);
	}

	// 出队
	public E deQueue() {
		return list.remove(0);
	}

	// 看一眼头部数据
	public E peekFront() {
		return list.get(0);
	}
	
	//清空队列元素
	public void clear() {
		list.clear();
	}
}

6.3 LinkedList模拟双端队列

deque => double ended queue

双端队列是能在头尾两端添加、删除的队列。

clipboard
public class LinkedListDeque<E> {
	private List<E> list = new LinkedList<E>();

	// 元素的数量
	public int size() {
		return list.size();
	}

	// 判断队列是否为空
	public boolean isEmpty() {
		return list.isEmpty();
	}

	// 从队尾入队
	public void enQueueRear(E element) {
		list.add(element);
	}

	// 从队头出队
	public E deQueueFront() {
		return list.remove(0);
	}

	// 从队头入队
	public void enQueueFront(E element) {
		list.add(0,element);
	}

	// 从队尾出队
	public E deQueueRear() {
		return list.remove(list.size() - 1);
	}

	// 看一眼头部数据
	public E peekront() {
		return list.get(0);
	}
	
	// 看一眼尾部数据
	public E peekRear() {
		return list.get(list.size() - 1);
	}

	// 清空队列元素
	public void clear() {
		list.clear();
	}
}

6.4 ArrayList模拟循环队列

其实队列底层也可以使用动态数组(ArrayList)实现,并且采用循环队列的方式各项接口也可以优化到 O(1) 的时间复杂度,这个用数组实现并且优化之后的队列也叫做:循环队列。

@SuppressWarnings("unchecked")
public class ArrayListCircleQueue<E> {
	private int front;//存储队头下标
	private E[] elements;
	private static final int DEFAULT_CAPACITY = 10;
	private int size;

	
	public ArrayListCircleQueue() {
		elements = (E[]) new Object[DEFAULT_CAPACITY];
	}
	
	/**
	 * @Description 数组容量不够则扩容
	 */
	private void ensureCapacity(int capacity) {
		int oldCapacity = elements.length;
		if(capacity <= oldCapacity) return;
		int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍
		E[] newElements = (E[])new Object[newCapacity];
		for (int i = 0; i < size; i++) {
			newElements[i] = elements[getRealIndex(i)];
		}
		elements = newElements;
		//重置front
		front = 0;
		System.out.println("扩容:" + oldCapacity + "=>" + newCapacity);
	}
	
	/**
	 * @Description 根据传入索引获取循环队列真实索引
	 * @param index
	 * @return
	 */
	private int getRealIndex(int index) {
		//注意:
		//  ① 尽量避免使用乘,除,模,浮点数运算,效率低下。
		//  ② 循环队列不会出现index为负数的情况,双端循环队列才会。
		//return (front + index) % elements.length;
		index += front;
		//注意使用此方法要保证index不会大于等于element.length的两倍。
		return index - (index >= elements.length ? elements.length : 0);
	}
	
	// 元素的数量
	public int size() {
		return size;
	}

	// 判断队列是否为空
	public boolean isEmpty() {
		return size == 0;
	}

	// 入队
	public void enQueue(E element) {
		ensureCapacity(size + 1);
		elements[getRealIndex(size)] = element;
		size++;
	}

	// 出队
	public E deQueue() {
		E frontElement = elements[front];
		elements[front] = null;
		front = getRealIndex(1);
		size--;
		return frontElement;
	}

	// 显示队列的头数据
	public E peek() {
		if(isEmpty()) {
			throw new RuntimeException("队列空,没有数据!");
		}
		return elements[front];
	}

	// 清空队列元素
	public void clear() {
		for (int i = 0; i < size; i++) {
			elements[getRealIndex(i)] = null;
		}
		size = 0;
		front = 0;
		if(elements != null && elements.length > DEFAULT_CAPACITY) {
			elements = (E[]) new Object[DEFAULT_CAPACITY];
		}
	}
	
	@Override
	public String toString() {
		StringBuilder str = new StringBuilder();
		str.append("capcacity=").append(elements.length)
		.append(" front=").append(front)
		.append(" size=").append(size).append(",[");
		for(int i = 0;i < elements.length;i++) {
			if(i != 0) {
				str.append(",");
			}
			str.append(elements[i]);
		}
		str.append("]");
		return str.toString();
	}
}

6.5 ArrayList模拟循环双端队列

@SuppressWarnings("unchecked")
public class ArrayListCircleDeque<E> {
	private int front;//存储队头下标
	private E[] elements;
	private static final int DEFAULT_CAPACITY = 10;
	private int size;
	
	public ArrayListCircleDeque() {
		elements = (E[]) new Object[DEFAULT_CAPACITY];
	}
	
	/**
	 * @Description 数组容量不够则扩容
	 */
	private void ensureCapacity(int capacity) {
		int oldCapacity = elements.length;
		if(capacity <= oldCapacity) return;
		int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍
		E[] newElements = (E[])new Object[newCapacity];
		for (int i = 0; i < size; i++) {
			newElements[i] = elements[getRealIndex(i)];
		}
		elements = newElements;
		//重置front
		front = 0;
		System.out.println("扩容:" + oldCapacity + "=>" + newCapacity);
	}
	
	/**
	 * @Description 根据传入索引获取循环队列真实索引
	 * @param index
	 * @return
	 */
	private int getRealIndex(int index) {
		index += front;
		if(index < 0) {
			return index + elements.length;
		}
		return index - (index >= elements.length ? elements.length : 0);
	}
	
	// 元素的数量
	public int size() {
		return size;
	}

	// 判断队列是否为空
	public boolean isEmpty() {
		return size == 0;
	}

	// 从队尾入队
	public void enQueueRear(E element) {
		ensureCapacity(size + 1);
		elements[getRealIndex(size)] = element;
		size++;
	}

	// 从队头出队
	public E deQueueFront() {
		E frontElement = elements[front];
		elements[front] = null;
		front = getRealIndex(1);
		size--;
		return frontElement;
	}

	// 从队头入队
	public void enQueueFront(E element) {
		ensureCapacity(size + 1);
		front = getRealIndex(-1);
		elements[front] = element;
		size++;
	}

	// 从队尾出队
	public E deQueueRear() {
		int realIndex = getRealIndex(size - 1);
		E rearElement = elements[realIndex];
		elements[realIndex] = null;
		size--;
		return rearElement;
	}

	// 显示队列的头数据,注意不是取出数据
	public E front() {
		return elements[front];
	}

	// 显示队尾数据
	public E rear() {
		return elements[getRealIndex(size - 1)];
	}

	// 清空队列元素
	public void clear() {
		for (int i = 0; i < size; i++) {
			elements[getRealIndex(i)] = null;
		}
		size = 0;
		front = 0;
		if(elements != null && elements.length > DEFAULT_CAPACITY) {
			elements = (E[]) new Object[DEFAULT_CAPACITY];
		}
	}
	
	@Override
	public String toString() {
		StringBuilder str = new StringBuilder();
		str.append("capcacity=").append(elements.length)
		.append(" front=").append(front)
		.append(" size=").append(size).append(",[");
		for(int i = 0;i < elements.length;i++) {
			if(i != 0) {
				str.append(",");
			}
			str.append(elements[i]);
		}
		str.append("]");
		return str.toString();
	}
}

四. 递归

1. 理解

递归就是 方法自己调用自己,每次调用时传入不同的变量。递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。

clipboard (1)

2. 应用场景

  • 各种数学问题如: 8皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google编程大赛)
  • 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等
  • 用栈解决的问题 => 使用递归可以让代码更简洁

3. 递归需要遵守的重要规则

  • 执行一个方法时,就创建一个 新的受保护的独立空间(栈空间)。
  • 方法的 局部变量是独立 的,不会相互影响, 比如上述n变量。
  • 如果方法中使用的是引用类型变量(比如数组),就会 共享该引用类型的数据
  • 递归 必须向退出递归的条件逼近,否则就是无限递归,出现StackOverflowError栈溢出了。
  • 当一个方法执行完毕,或者遇到return,就会返回,遵守 谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。

4. 递归的应用-迷宫问题

clipboard (2)
public class MazeRetrospective {
	public static void main(String[] args) {
		//创建一个二维数组,模拟迷宫地图
		int[][] map = new int[8][7];
		//使用1代表墙
		for(int i = 0;i < 7;i++) {
			map[0][i] = 1;
			map[7][i] = 1;
		}
		for(int i = 0;i < 8;i++) {
			map[i][0] = 1;
			map[i][6] = 1;
		}
		//设置挡板
		map[2][1] = 1;
		map[2][2] = 1;
		map[2][3] = 1;
		map[5][4] = 1;
		map[5][5] = 1;
		//输出地图
		System.out.println("初始化地图:");
		for(int i = 0;i < 8;i++) {
			for(int j = 0;j < 7;j++) {
				System.out.print(map[i][j] + " ");
			}
			System.out.println();
		}
	
		//使用递归回溯给小球找路
		setWay(map, 1, 1);
		System.out.println("小球走过后的地图:");
		for(int i = 0;i < 8;i++) {
			for(int j = 0;j < 7;j++) {
				System.out.print(map[i][j] + " ");
			}
			System.out.println();
		}
	}
	
	/**
	 * 使用递归回溯给小球找路 => 
	 * 说明:① 小球能到map[6][5]则说明通路找到。
	 * 		② 当map[i][j]为0表示该点没有走过,当为1表示墙,
	 *        3表示已经走过但是走不通。
	 *      ③ 走迷宫时,需要确定一个策略(方法):下=>右=>上=>左,
	 *        如果走不通再回溯
	 * @param map 表示地图
	 * @param i 表示从哪里走:(i,j)
	 * @param j 表示从哪里走:(i,j)
	 * @return 如果找到通路返回true,否则返回false
	 */
	public static boolean setWay(int[][] map,int i,int j) {
		if(map[6][5] == 2) {
			//通路已经找到
			return true;
		} else {
			//如果当前点还没有走过
			if(map[i][j] == 0) {
				//按照策略走
				map[i][j] = 2;//假定该点可以走通
				if(setWay(map,i + 1,j)) {
					//向下能走通
					return true;
				} else if(setWay(map,i,j + 1)) {
					//向右能走通
					return true;
				} else if(setWay(map,i - 1,j)) {
					//向上能走通
					return true;
				} else if(setWay(map,i,j - 1)) {
					//向左能走通
					return true;
				} else {
					//不能走通,该点是死路
					map[i][j] = 3;
					return false;
				}
			} else {
				//如果map[i][j]不等0,则可能是1,2,3
				return false;
			}
		}
	}
}
//结果:
//  1 1 1 1 1 1 1 
//  1 2 2 2 2 3 1 
//  1 1 1 1 2 3 1 
//  1 0 0 0 2 3 1 
//  1 0 0 2 2 3 1 
//  1 0 0 2 1 1 1 
//  1 0 0 2 2 2 1 
//  1 1 1 1 1 1 1 

5. 递归的应用-八皇后问题(回溯算法)

clipboard (3)

思路分析

  • 第一个皇后先放第一行第一列。

  • 第二个皇后放在第二行第一列、然后判断是否OK, 如果不OK,继续放在第二列、第三列、依次把所有列都放完,找到一个合适的。

  • 继续第三个皇后,还是第一列、第二列……直到第8个皇后也能放在一个不冲突的位置,算是找到了一个正确解。

  • 当得到一个正确解时,在栈回退到上一个栈时就会开始回溯(一层一层的回溯)。即将第一个皇后放到第一列的所有正确解,全部得到。

  • 然后回头继续第一个皇后放第二列,后面继续循环执行上面的步骤。

说明:理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题. arr[8] = {0 , 4, 7, 5, 2, 6, 1, 3} //对应arr 下标 表示第几行,即第几个皇后,arr[i] = val , val 表示第i+1个皇后,放在第i+1行的第val+1列。

实现

public class EightQueensHhess {
	int max = 8;//定义有多少个皇后
	int[] array = new int[max];//数组array保存皇后放置位置的结果
	static int count = 0;
	static int judgeCount = 0;
	public static void main(String[] args) {
		EightQueensHhess queen = new EightQueensHhess();
		queen.check(0);
		System.out.println("一共有 " + count + " 种解法");
		System.out.println("整个过程判断冲突的次数为 " + judgeCount + " 次");
	}
	
	//放置第n个皇后的方法
	//注意:check每一次递归时,进入check中都有for(int i = 0;i < max;i++),
	//      因此会进行回溯。
	private void check(int n) {
		if(n == max) {
			//n从零开始,当n为8时就已经将8个皇后放置好了
			show();
			return;
		}
		//依次放入皇后并判断是否冲突
		for(int i = 0;i < max;i++) {
			//先把当前这个皇后n放到该行第一列
			array[n] = i;
			//判断当放置第n个皇后到i列时是否冲突
			if(judge(n)) {
				//如果不冲突就放第n+1个皇后,即开始递归
				check(n+1);
			}
			//注意:如果冲突就继续执行array[n] = i,即将第n个皇后放置
			//     在本行后移的一个位置。
		}
	}
	
	/**
	 * @Description 查看当我们放置第n个皇后时,就去检测该皇后和前面已经
	 * 				摆放的皇后是否存在冲突。
	 * @param n 表示第n个皇后
	 * @return
	 */
	private boolean judge(int n) {
		judgeCount++;
		for(int i = 0;i < n;i++) {
			//1.array[i] == array[n]表示判断第n个皇后是否和前面的n-1个
			//  皇后在同一列。
			//2.Math.abs(n-i) == Math.abs(array[n] - array[i]表示判断
			//  第n个皇后是否和第i个皇后是否在同一斜线。
			//3.没有必要判断是否在同一行,因为n每次都会递增。
			if(array[i] == array[n] || 
					Math.abs(n-i) == Math.abs(array[n] - array[i])) {
				return false;
			}
		}
		return true;
	}
	
	//将皇后摆放的位置输出的方法
	private void show() {
		count++;
		for (int i = 0; i < array.length; i++) {
			System.out.print(array[i] + " ");
		}
		System.out.println();
	}
}

五. 树形结构

1. 概述

基本概念

  • 节点根节点父节点子节点兄弟节点(有相同的父节点)
  • 一棵树可以没有任何节点,称为 空树
  • 一棵树可以只有 1 个节点,也就是 只有根节点
  • 子树左子树右子树
  • 节点的度(degree):子树的个数
  • 树的度:所有节点度中的最大值
  • 叶子节点(leaf):度为 0 的节点
  • 非叶子节点:度不为 0 的节点
  • 层数(level):根节点在第 1 层,根节点的子节点在第 2 层,以此类推(有些说法也从第 0 层开始计算)
  • 节点的深度(depth):从根节点到当前节点的唯一路径上的节点总数
  • 节点的高度(height):从当前节点到最远叶子节点的路径上的节点总数
  • 树的深度:所有节点深度中的最大值
  • 树的高度:所有节点高度中的最大值
  • 树的深度 等于 树的高度
  • 树支路总数 = 树节点总数 - 1 (树中每个节点头上都有一个支路,但唯独根节点没有)

有序树,无序树,森林

  • 有序树:树中任意节点的子节点之间有顺序关系
  • 无序树:树中任意节点的子节点之间没有顺序关系,也称为“自由树”
  • 森林:由 m(m ≥ 0)棵互不相交的树组成的集合

2. 二叉树

2.1 特点

  • 每个节点的度最大为 2(最多拥有 2 棵子树)
  • 左子树和右子树是有顺序的
  • 即使某节点只有一棵子树,也要区分左右子树
  • 二叉树是度不大于2的有序树。但是度不大于2的有序树不是二叉树(因为有序树的节点次序是相对于另一节点而言的,当有序树的子树中只有一个孩子时,这个孩子节点无需区分左右次序,而二叉树无论孩子树是否为2,均需要确定左右次序)。

2.2 性质

  • 非空二叉树的第 i 层,最多有 2^( i − 1) 个节点( i ≥ 1 )
  • 在高度为 h 的二叉树上最多有 2^h − 1 个结点( h ≥ 1 )
// S=2^0 + 2^1 + 2^2 + 2^3 +..+ 2^(n-1)
// 2S=2^1 + 2^2 + 2^3 +..+ 2^(n-1) + 2^n
// 两式相减
// 2S-S=2^n - 2^0
// S=2^n - 1
  • 对于任何一棵非空二叉树,如果叶子节点个数为 n0,度为 2 的节点个数为 n2,则有: n0 = n2 + 1
//推导步骤:
// ① 假设度为 1 的节点个数为 n1,那么二叉树的节点总数 n = n0 + n1 + n2 
// ② 二叉树的支路数 T = n1 + 2 * n2 = n – 1 = n0 + n1 + n2 – 1 
// ③ 因此 n0 = n2 + 1

2.3 真二叉树

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

clipboard (4)

2.4 满二叉树

理解:最后一层节点的度都为 0,其他节点的度都为 2。

注意:

① 在同样高度的二叉树中,满二叉树的叶子节点数量最多、总节点数量最多。

② 满二叉树一定是真二叉树,真二叉树不一定是满二叉树。

clipboard (5)

2.5 完全二叉树

理解:对节点从上至下、左至右开始编号,其所有编号都能与相同高度的满二叉树中的编号对应。

注意:

① 叶子节点只会出现最后 2 层,最后 1 层的叶子结点都靠左对齐。

② 完全二叉树从根结点至倒数第 2 层是一棵满二叉树。

③ 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。

clipboard (6)

性质:

// 1. 度为 1 的节点只有左子树。
// 2. 度为 1 的节点要么是 1 个,要么是 0 个
// 3. 同样节点数量的二叉树,完全二叉树的高度最小
// 4. 假设完全二叉树的高度为 h(h ≥ 1),那么:
//    ① 至少有 2^(h−1) 个节点 (2^0 + 2^1 + 2^2 + ⋯ + 2^(h−2) + 1 )
//    ② 最多有 2^h − 1 个节点(2^0 + 2^1 + 2^2 + ⋯ + 2^(h−1),满二叉树)
//    ③ 当总节点数量为 n,
//      可得:2^(h-1) <= n < 2^h
//      =》   h - 1 <= logn < h
//     =》   h = floor(logn) + 1  ※  //floor:向下取整 ceiling:向上取整
// 5. 一棵有 n 个节点的完全二叉树(n > 0),从上到下、从左到右对节点从 1 开始
//    进行编号,对任意第 i 个节点:
//    ① 如果 i = 1 ,它是根节点
//    ② 如果 i > 1 ,它的父节点编号为 floor( i / 2 )
//    ③ 如果 2i ≤ n ,它的左子节点编号为 2i
//    ④ 如果 2i > n ,它无左子节点
//    ⑤ 如果 2i + 1 ≤ n ,它的右子节点编号为 2i + 1
//    ⑥ 如果 2i + 1 > n ,它无右子节点
// 6. 一棵有 n 个节点的完全二叉树(n > 0),从上到下、从左到右对节点从 0 
//   开始进行编号,对任意第 i 个节点:
//   ① 如果 i = 0 ,它是根节点
//   ② 如果 i > 0 ,它的父节点编号为 floor( (i – 1) / 2 )
//   ③ 如果 2i + 1 ≤ n – 1 ,它的左子节点编号为 2i + 1
//   ④ 如果 2i + 1 > n – 1 ,它无左子节点
//   ⑤ 如果 2i + 2 ≤ n – 1 ,它的右子节点编号为 2i + 2
//   ⑥ 如果 2i + 2 > n – 1 ,它无右子节点

面试题:如果一棵完全二叉树有 768 个节点,求叶子节点的个数

// 解: 设叶子节点为 n0,度为2的节点为 n2,度为1的节点为 n1
//     n = n0 + n1 + n2
//     n0 = n2 + 1
//     => n = 2n0 + n1 - 1
// 又因 完全二叉树度为1的节点要么是 1 个,要么是 0 个
//     ① n1为1 时,n = 2n0,n必然为偶数。
//     ② n1为0时,n = 2n0 - 1,n必然为奇数。
//     => n0 = 768 / 2 = 384

由以上题总结公式:

  • 当总节点为偶数,n0 = n / 2。

  • 当总节点数为奇数,n0 = (n + 1) / 2

  • => n0 = floor( (n + 1) / 2 ) = ceiling( n / 2) 注意:java除法默认向下取整

判断一棵树是否为完全二叉树:

  • 思路一:
image-20211011003318480
  • 思路二:
image-20211011004513101

2.6 二叉树的遍历

遍历是数据结构中的常见操作:把所有元素都访问一遍

线性数据结构的遍历比较简单

  • 正序遍历
  • 逆序遍历

根据节点访问顺序的不同,二叉树的常见遍历方式有4种

  • 前序遍历(Preorder Traversal)
  • 中序遍历(Inorder Traversal)
  • 后序遍历(Postorder Traversal)
  • 层序遍历(Level Order Traversal)

前序遍历(递归/迭代)

访问顺序 :根节点、前序遍历左子树、前序遍历右子树

应用:树状结构展示(注意左右子树的顺序)

clipboard (7)

中序遍历(递归/迭代)

访问顺序 :中序遍历左子树、根节点、中序遍历右子树

应用:二叉搜索树的中序遍历按升序或降序处理节点

clipboard (8)

后序遍历(递归 / 迭代)

访问顺序 :后序遍历左子树、后序遍历右子树、根节点

应用:适用于一些先子后父的操作

clipboard (9)

层序遍历 (迭代实现:队列)

//实现思路
// 1. 将根节点入队
// 2. 循环执行以下操作,直到队列为空
//    ① 将队头节点 A 出队,进行访问
//    ② 将 A 的左子节点入队
//    ③ 将 A 的右子节点入队

访问顺序 :从上到下、从左到右依次访问每一个节点

应用:① 计算二叉树的高度 ② 判断一棵树是否为完全二叉树

clipboard (10)

2.7 表达式树

四则运算的表达式可以分为3种

  • 前缀表达式(prefix expression),又称为波兰表达式
  • 中缀表达式(infix expression)
  • 后缀表达式(postfix expression),又称为逆波兰表达式
image-20220106233403750

如果将表达式的操作数作为叶子节点,运算符作为父节点(假设只是四则运算)

  • 这些节点刚好可以组成一棵二叉树
  • 比如表达式:A / B + C * D – E
image-20220106233524304

如果对这棵二叉树进行遍历

前序遍历

  • - + / A B * C D E
  • 刚好就是前缀表达式(波兰表达式)

中序遍历

  • A / B + C * D – E
  • 刚好就是中缀表达式(波兰表达式)

后序遍历

  • A B / C D * + E –
  • 刚好就是后缀表达式(逆波兰表达式)

2.8 二叉树遍历的应用

  • 前序遍历:树状结构展示(注意左右子树的顺序)
  • 中序遍历:二叉搜索树的中序遍历按升序或者降序处理节点
  • 后序遍历:适用于一些先子后父的操作
  • 层序遍历:①计算二叉树的高度。②判断一棵树是否为完全二叉树

2.9 根据遍历结果重构二叉树

  • 前序遍历 + 中序遍历 => 唯一的一颗二叉树
  • 后序遍历 + 中序遍历 => 唯一的一颗二叉树
  • 前序遍历 + 后序遍历 => 如果它是一棵真二叉树,结果是唯一的 。否则不然结果不唯一 。

2.10 前驱节点(predecessor)

理解:中序遍历时的前一个节点 “删除节点要使用该知识”

如果是二叉搜索树,前驱节点就是前一个比它小的节点

clipboard

2.11 后继节点(successor)

理解:中序遍历时的后一个节点 “删除节点要使用该知识”

如果是二叉搜索树,后继节点就是后一个比它大的节点

clipboard (1)

2.12 打印二叉树的工具

https://github.com/CoderMJLee/BinaryTrees

使用步骤:

  • 实现 BinaryTreeInfo 接口
  • 调用打印API :BinaryTrees.println(bst);

2.13 二叉树遍历非递归实现思路

前序遍历-非递归

利用栈实现(法一)

  • 设置 node = root
  • 循环执行以下操作
    • 如果 node != null
      • 对 node 进行访问
      • 将 node.right 入栈
      • 设置 node = node.left
    • 如果 node == null
      • 如果栈为空,结束遍历
      • 如果栈不为空,弹出栈顶元素并赋值给 node

利用栈实现(法二)

  • 将 root 入栈

  • 循环执行以下操作,直到栈为空

  • 弹出栈顶节点 top,进行访问

  • 将 top.right 入栈

  • 将 top.left 入栈

中序遍历-非递归

利用栈实现 :

  • 设置 node = root

  • 循环执行以下操作

    • 如果 node != null
      • 将 node 入栈
      • 设置 node = node.left
    • 如果 node == null
      • 如果栈为空,结束遍历
      • 如果栈不为空,弹出栈顶元素并赋值给 node
        • 对 node 进行访问
        • 设置 node = node.right

后序遍历 – 非递归

利用栈实现

  • 将 root 入栈
  • 循环执行以下操作,直到栈为空
    • 如果栈顶节点是叶子节点 或者 上一次访问的节点是栈顶节点的子节点 => 弹出栈顶节点,进行访问
    • 否则 => 将栈顶节点的right、left按顺序入栈

2.14 二叉树代码实现

/**
 * @Description 二叉树
 * @Author monap
 * @Date 2021/10/10 20:10
 */
@SuppressWarnings("unchecked")
public class BinaryTree<E> implements BinaryTreeInfo {
    protected int size;
    /**
     * 根节点
     */
    protected Node<E> root;

    /**
     * 节点内部类
     */
    protected static class Node<E> {
        E element;
        Node<E> left; // 左子节点
        Node<E> right; // 右子节点
        Node<E> parent; // 父节点

        /**
         * 左右节点可能没有,不必须
         */
        public Node(E element, Node<E> parent) {
            this.element = element;
            this.parent = parent;
        }

        public boolean isLeaf() {
            return left == null && right == null;
        }

        public boolean hasTwoChildren() {
            return left != null && right != null;
        }

        public boolean isLeftChild() {
            return parent != null && this == parent.left;
        }

        public boolean isRightChild() {
            return parent != null && this == parent.right;
        }

        public Node<E> getSibling() {
            if (isLeftChild()) {
                return parent.right;
            }
            if (isRightChild()) {
                return parent.left;
            }
            return null;
        }

        @Override
        public String toString() {
            String parentStr = "null";
            if (parent != null) {
                parentStr = parent.element.toString();
            }
            return element + "_P(" + parentStr + ")";
        }
    }

    protected Node<E> createNode(E element, Node<E> parent) {
        return new Node<>(element, parent);
    }

    /**
     * 元素的数量
     */
    public int size() {
        return size;
    }

    /**
     * 是否为空
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 对外接口,用于传出去遍历到的元素(类似于Comparator定制排序)
     */
    public static abstract class Visitor<E> {
        boolean stop;

        // 如果返回true就停止遍历
        public abstract boolean visit(E element);
    }

    /**
     * 前序遍历(非递归方式)、使用栈实现
     */
    public void preorderTraversal(Visitor<E> visitor) {
        if (visitor == null || root == null) return;
        Node<E> node = root;
        Stack<Node<E>> stack = new Stack<>();
        while (true) {
            if (node != null) {
                // 访问node节点
                if (visitor.visit(node.element)) return;
                // 将右子节点入栈
                if (node.right != null) {
                    stack.push(node.right);
                }
                // 向左左
                node = node.left;
            } else if (stack.isEmpty()) {
                return;
            } else {
                // 处理右边
                node = stack.pop();
            }
        }
    }

    /**
     * 前序遍历(递归方式)
     */
    public void preorderTraversal2(Visitor<E> visitor) {
        if (visitor == null) return;
        preorderTraversal(root, visitor);
    }

    protected void preorderTraversal(Node<E> node, Visitor<E> visitor) {
        if (node == null || visitor.stop) {
            return;
        }
        // System.out.println(node.element);
        // visitor.visit(node.element);//定制
        // if(visitor.stop) return;
        visitor.stop = visitor.visit(node.element);
        preorderTraversal(node.left, visitor);
        preorderTraversal(node.right, visitor);
    }

    /**
     * 中序遍历(非递归方式)、利用栈实现
     */
    public void inorderTraversal(Visitor<E> visitor) {
        if (visitor == null || root == null) return;
        Node<E> node = root;
        Stack<Node<E>> stack = new Stack<>();
        while (true) {
            if (node != null) {
                stack.push(node);
                node = node.left;
            } else if (stack.isEmpty()) {
                return;
            } else {
                node = stack.pop();
                // 访问node节点
                if (visitor.visit(node.element)) return;
                // 让右节点进行中序遍历
                node = node.right;
            }
        }
    }

    /**
     * 中序遍历(递归方式)
     */
    public void inorderTraversal2(Visitor<E> visitor) {
        if (visitor == null) {
            return;
        }
        inorderTraversal(root, visitor);
    }

    protected void inorderTraversal(Node<E> node, Visitor<E> visitor) {
        if (node == null || visitor.stop) {
            return;
        }
        inorderTraversal(node.left, visitor);
        // visitor.visit(node.element);
        if (visitor.stop) {
            return;
        }
        visitor.stop = visitor.visit(node.element);
        inorderTraversal(node.right, visitor);
    }

    /**
     * 后序遍历(非递归方式)、利用栈实现
     */
    public void postorderTraversal(Visitor<E> visitor) {
        if (visitor == null || root == null) return;
        // 记录上一次弹出访问的节点
        Node<E> prev = null;
        Stack<Node<E>> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            Node<E> top = stack.peek();
            if (top.isLeaf() || (prev != null && prev.parent == top)) {
                prev = stack.pop();
                // 访问node节点
                if (visitor.visit(prev.element)) return;
            } else {
                if (top.right != null) {
                    stack.push(top.right);
                }
                if (top.left != null) {
                    stack.push(top.left);
                }
            }
        }
    }

    /**
     * 后序遍历(递归方式)
     */
    public void postorderTraversal2(Visitor<E> visitor) {
        if (visitor == null) {
            return;
        }
        postorderTraversal(root, visitor);
    }

    protected void postorderTraversal(Node<E> node, Visitor<E> visitor) {
        if (node == null || visitor.stop) {
            // visitor.stop中止递归
            return;
        }
        postorderTraversal(node.left, visitor);
        postorderTraversal(node.right, visitor);
        // visitor.visit(node.element);
        if (visitor.stop) {
            return; // 中止当前打印 ↓
        }
        visitor.stop = visitor.visit(node.element);
    }

    /**
     * 层序遍历(迭代:队列)
     */
    public void levelOrderTraversal(Visitor<E> visitor) {
        if (root == null || visitor == null) {
            return;
        }
        Queue<Node<E>> queue = new LinkedList<>();
        // 将根节点入队
        queue.offer(root);
        while (!queue.isEmpty()) {
            // 将头节点出队
            Node<E> node = queue.poll();
            // System.out.println(node.element);
            // visitor.visit(node.element);
            if (visitor.visit(node.element)) {
                return;
            }
            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
    }

    /**
     * 清空所有元素
     */
    public void clear() {
        root = null;
        size = 0;
    }

    //---------------------------

    /**
     * 利用前序遍历进行简单的树状结构展示
     */
    @Override
    public String toString() {
        StringBuilder str = new StringBuilder();
        toString(root, str, "");
        return str.toString();
    }

    protected void toString(Node<E> node, StringBuilder str, String prefix) {
        if (node == null) {
            return;
        }
        str.append(prefix).append(node.element).append("\n");
        toString(node.left, str, prefix + "L-");
        toString(node.right, str, prefix + "R-");
    }

    //---------------------------

    /**
     * 利用中序遍历求某个节点的前驱节点
     */
    protected Node<E> predecessor(Node<E> node) {
        if (node == null) {
            return null;
        }
        // 前驱节点在左子树中:node.left.right.right...
        Node<E> p = node.left;
        if (p != null) {
            while (p.right != null) {
                p = p.right;
            }
            return p;
        }
        // 从祖父节点中寻找前驱节点
        while (node.parent != null && node == node.parent.left) {
            node = node.parent;
        }
        // 情况一:node.parent == null ↓
        // 情况二:node == node.parent.right ↓
        return node.parent;
    }

    /**
     * 利用中序遍历求某个节点的后继节点
     */
    protected Node<E> successor(Node<E> node) {
        if (node == null) {
            return null;
        }
        // 前驱节点在右子树中:node.right.left.left...
        Node<E> p = node.right;
        if (p != null) {
            while (p.left != null) {
                p = p.left;
            }
            return p;
        }
        // 从祖父节点中寻找前驱节点
        while (node.parent != null && node == node.parent.right) {
            node = node.parent;
        }
        // 情况一:node.parent == null ↓
        // 情况二:node == node.parent.left ↓
        return node.parent;
    }

    //---------------------------

    /**
     * 计算二叉树的高度(递归)
     */
    public int heightByRecursion() {
        return height(root);
    }

    /**
     * 获取某一个节点的高度
     */
    protected int height(Node<E> node) {
        if (node == null) {
            return 0;
        }
        return 1 + Math.max(height(node.left), height(node.right));
    }

    /**
     * 利用层序遍历计算二叉树的高度(迭代)
     */
    public int heightByLevelOrderTraversal() {
        if (root == null) {
            return 0;
        }
        int height = 0;
        // 存储着每一层的元素数量
        int levelSize = 0;
        Queue<Node<E>> queue = new LinkedList<>();
        // 将根节点入队
        queue.offer(root);
        while (!queue.isEmpty()) {
            levelSize = queue.size();
            for (int i = 0; i < levelSize; i++) {
                Node<E> node = queue.poll();
                assert node != null;
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }
            height++;
        }
        return height;
    }

    /**
     * 利用层序遍历判断一颗树是否为完全二叉树
     */
    public boolean isComplete() {
        if (root == null) {
            return false;
        }
        Queue<Node<E>> queue = new LinkedList<>();
        // 将根节点入队
        queue.offer(root);
        boolean leaf = false;
        while (!queue.isEmpty()) {
            // 将头节点出队
            Node<E> node = queue.poll();
            if (leaf && !node.isLeaf()) {
                return false;
            }

            if (node.left != null) {
                queue.offer(node.left);
            } else if (node.right != null) {
                //node.left == null && node.right != null
                return false;
            }

            if (node.right != null) {
                queue.offer(node.right);
            } else {
                //node.right == null
                leaf = true;
            }
        }
        return true;
    }

    /**
     * 使用前序遍历翻转二叉树(所有遍历方式都可实现)
     *
     * @param root
     * @return
     */
    public Node<E> invertTree(Node<E> root) {
        if (root == null) {
            return null;
        }
        Node<E> tmp = root.left;
        root.left = root.right;
        root.right = tmp;
        invertTree(root.left);
        invertTree(root.right);
        return root;
    }

    //---------------------------

    /**
     * 实现BinaryTreeInfo接口,进行高级的树状结构展示
     */
    @Override
    public Object root() {
        return root;
    }

    @Override
    public Object left(Object node) {
        return ((Node<E>) node).left;
    }

    @Override
    public Object right(Object node) {
        return ((Node<E>) node).right;
    }

    @Override
    public Object string(Object node) {
        return node;
    }
}

3. 二叉搜索树

3.1 引入

在 n 个动态的整数中搜索某个整数?(查看其是否存在)。

  • 假设使用动态数组存放元素,从第 0 个位置开始遍历搜索,平均时间复杂度:O(n)。

    • 如果维护一个有序的动态数组,使用二分搜索,最坏时间复杂度:O(logn)。但是添加、删除的平均时间复杂度是 O(n)。

针对这个需求,有没有更好的方案?=> 使用二叉搜索树,添加、删除、搜索的最坏时间复杂度均可优化至:O(logn)级别 <==> O(h) 复杂度只与h有关

clipboard (2) clipboard (3)

3.2 理解

二叉搜索树是二叉树的一种,是应用非常广泛的一种二叉树,英文简称为 BST。

  • 又被称为:二叉查找树、二叉排序树

  • 任意一个节点的值都大于其左子树所有节点的值

  • 任意一个节点的值都小于其右子树所有节点的值

  • 它的左右子树也是一棵二叉搜索树

二叉搜索树可以大大提高搜索数据的效率

二叉搜索树存储的元素必须具备可比较性

  • 比如 int、double 等
  • 如果是自定义类型,需要指定比较方式
  • 不允许为 null
clipboard (4)

3.3 接口设计

由于二叉搜索树继承于二叉树,只需要实现添加,删除,包含的接口

clipboard (5)

注意:对于当前使用的二叉树来说,它的元素没有索引的概念。

3.4 图解

添加节点

clipboard (6)

删除节点

clipboard (7) clipboard (8) clipboard (9)

3.5 代码实现

/**
 * @Description 二叉搜索树
 * @Author monap
 * @Date 2021/10/10 20:17
 */
@SuppressWarnings("unchecked")
public class BSTree<E> extends BinaryTree<E> {
    /**
     * 比较器定制排序
     */
    protected Comparator<E> comparator;

    public BSTree() {
        this(null);
    }

    public BSTree(Comparator<E> comparator) {
        this.comparator = comparator;
    }

    /**
     * 检查添加元素是否为空
     */
    protected void elementNoNullCheck(E element) {
        if (element == null) {
            throw new IllegalArgumentException("element must no be null!");
        }
    }

    /**
     * 比较元素大小,返回值为0代表e1等于e2,大于0代表e1大于e2,小于0代表e1小于e2
     */
    protected int compare(E e1, E e2) {
        if (comparator != null) {
            return comparator.compare(e1, e2);
        }
        // 注意:不在上面写死(BinarySearchTree<E extends Comparable>),
        // 而是在这里进行强制转换,如果E没有实现此接口就会报错提醒。否则
        // 如果E没有实现此接口在编译时就会报错,我们希望两种比较方式都可以使用。
        return ((Comparable<E>) e1).compareTo(e2);
    }

    /**
     * 添加元素
     */
    public void add(E element) {
        elementNoNullCheck(element);
        // 添加第一个节点(根节点)
        if (root == null) {
            root = createNode(element, null);
            size++;
            // 新添加节点之后的处理
            afterAdd(root);
            return;
        }
        // 如果添加的不是第一个节点:
        // 1.找到待添加位置的父节点
        Node<E> parent = root;
        Node<E> node = root;
        int cmp = 0;
        while (node != null) {
            cmp = compare(element, node.element);
            parent = node;
            if (cmp > 0) {
                node = node.right;
            } else if (cmp < 0) {
                node = node.left;
            } else {
                // 一般覆盖(不同对象可能有相同的比较参数)
                node.element = element;
                return;
            }
        }
        // 2.判断插入父节点的左子节点还是右子节点
        Node<E> newNode = createNode(element, parent);
        if (cmp > 0) {
            parent.right = newNode;
        } else {
            parent.left = newNode;
        }
        size++;
        // 新添加节点之后的处理
        afterAdd(newNode);
    }

    /**
     * 添加node节点后所需要做的调整(二叉搜索树不需要调整)
     */
    protected void afterAdd(Node<E> node) {
    }

    /**
     * 删除元素
     */
    public void remove(E element) {
        remove(node(element));
    }

    /**
     * 根据元素找到对应节点
     */
    protected Node<E> node(E element) {
        Node<E> node = root;
        while (node != null) {
            int cmp = compare(element, node.element);
            if (cmp == 0) {
                return node;
            }
            if (cmp > 0) {
                node = node.right;
            } else {
                node = node.left;
            }
        }
        return null;
    }

    /**
     * 删除对应节点
     */
    protected void remove(Node<E> node) {
        if (node == null) {
            return;
        }
        size--;
        // 考虑度为2的节点,转化为度为1
        if (node.hasTwoChildren()) {
            // 后继节点
            Node<E> s = successor(node);
            // 用后继节点的值覆盖度为2的节点的值
            node.element = s.element;
            // 删除后继节点
            node = s;
        }
        // 删除node节点(能到这则说明node的度必为0或1)
        Node<E> replacement = node.left != null ? node.left : node.right;
        // node是度为1的节点
        if (replacement != null) {
            //更改parent
            replacement.parent = node.parent;
            // 更改parent的left,right指向
            // node是度为1的节点也是根节点
            if (node.parent == null) {
                root = replacement;
            } else if (node == node.parent.left) {
                node.parent.left = replacement;
            } else {
                // 在右边
                node.parent.right = replacement;
            }
            // 此时开始恢复平衡(AVL树 或 RB树需要实现此方法)
            afterRemove(node, replacement);
        } else if (node.parent == null) {
            // node是叶子节点也是根节点
            root = null;
            afterRemove(node, null);
        } else {
            // node是叶子节点但不是根节点
            if (node == node.parent.left) {
                node.parent.left = null;
            } else {
                node.parent.right = null;
            }
            // 此时开始恢复平衡(AVL树 或RB树 需要实现此方法)
            afterRemove(node, null);
        }
    }

    /**
     * 删除node节点后所需要做的调整(二叉搜索树不需要调整)
     */
    protected void afterRemove(Node<E> node, Node<E> replacement) {
    }

    /**
     * 是否包含某元素
     */
    public boolean contains(E element) {
        return node(element) != null;
    }
}

3.6 测试

public class BSTreeTest {
	@Test
	public void test() {
		//测试二叉树打印工具
		//  ┌─_A_─┐
		//  │     │
		// _B_   _C_
		BinaryTrees.println(new BinaryTreeInfo() {
			@Override
			public Object string(Object node) {
				return "_" + node.toString() + "_";
			}
			
			@Override
			public Object root() {
				return "A";
			}
			
			@Override
			public Object right(Object node) {
				if(node.equals("A")) return "C";
				return null;
			}
			
			@Override
			public Object left(Object node) {
				if(node.equals("A")) return "B";
				return null;
			}
		});
	}
	
	@Test
	public void test1() {
		// 自然排序
		Integer[] data = new Integer[] { 7, 4, 9, 2, 5, 8, 11, 3, 12, 1 };
		BSTree<Integer> bst = new BSTree<>();
		for (int i = 0; i < data.length; i++) {
			bst.add(data[i]);
		}
		BinaryTrees.println(bst, PrintStyle.INORDER);
		String str = BinaryTrees.printString(bst);
		Files.writeToFile("D:/1.txt", str);
	}

	@Test
	public void test2() {
		// 定制排序
		Integer[] data = new Integer[] { 7, 4, 9, 2, 5, 8, 11, 3, 12, 1 };
		BSTree<Person> bst = new BSTree<>(new Comparator<Person>() {
			@Override
			public int compare(Person e1, Person e2) {
				return e2.getAge() - e1.getAge();
			}
		});
		for (int i = 0; i < data.length; i++) {
			bst.add(new Person(data[i]));
		}
		BinaryTrees.println(bst);
	}
	
	@Test
	public void test3() {
		//遍历测试
		Integer[] data = new Integer[] { 7, 4, 2, 1, 3, 5, 9, 8, 11, 10, 12};
		BSTree<Integer> bst = new BSTree<>();
		for (int i = 0; i < data.length; i++) {
			bst.add(data[i]);
		}
		BinaryTrees.println(bst);
		BinaryTrees.println(bst);
		//测试前序遍历(递归)
		System.out.println("前序遍历:");
		bst.preorderTraversal(new Visitor<Integer>() {
			public boolean visit(Integer element) {
				System.out.print(element + " ");
				return element == 2 ? true : false;//遍历到值为2停止
			}
		});
		System.out.println();
		//测试中序遍历(递归)
		System.out.println("中序遍历:");
		bst.inorderTraversal(new Visitor<Integer>() {
			public boolean visit(Integer element) {
				System.out.print(element + " ");
				return element == 4 ? true : false;//遍历到值为4停止
			}
		});
		System.out.println();
		//测试后序遍历(递归)
		System.out.println("后序遍历:");
		bst.postorderTraversal(new Visitor<Integer>() {
			public boolean visit(Integer element) {
				System.out.print(element + " ");
				return element == 4 ? true : false;//遍历到值为4停止
			}
		});
		System.out.println();
		//测试层序序遍历(队列)
		System.out.println("层序遍历:");
		bst.levelOrderTraversal(new Visitor<Integer>() {
			public boolean visit(Integer element) {
				System.out.print(element + "_ ");
				return false;//遍历所有
			}
		});
	}
	
	@Test
	public void test4() {
		//遍历的应用测试
		Integer[] data = new Integer[] { 7, 4, 9, 5, 2};
		BSTree<Integer> bst = new BSTree<>();
		for (int i = 0; i < data.length; i++) {
			bst.add(data[i]);
		}
		BinaryTrees.println(bst);
		//1.前序遍历的应用:展示树状结构(其他遍历方式也可以展示树状结构)
		System.out.println(bst);
		//2.层序遍历的应用:计算二叉树的高度
		System.out.println("二叉树高度为:" + bst.height1());//递归方式
		System.out.println("二叉树高度为:" + bst.height2());//层序递归方式
		//3.层序遍历的应用:判断一颗树是否是完全二叉树
		System.out.println("当前二叉搜索树是否为完全二叉树:" + bst.isComplete());
		Integer[] data1 = new Integer[] { 7, 4, 9, 2, 1};
		BSTree<Integer> bst1 = new BSTree<>();
		for (int i = 0; i < data1.length; i++) {
			bst1.add(data1[i]);
		}
		BinaryTrees.println(bst1);
		System.out.println("当前二叉搜索树是否为完全二叉树:" + bst1.isComplete());
	}
	
	@Test
	public void test5() {
		//测试删除
		Integer[] data = new Integer[] { 7, 4, 9, 2, 5, 8, 11, 3, 12, 1 };
		BSTree<Integer> bst = new BSTree<>();
		for (int i = 0; i < data.length; i++) {
			bst.add(data[i]);
		}
		BinaryTrees.println(bst);
		bst.remove(7);
		BinaryTrees.println(bst);
	}
}

class Person implements Comparable<Person> {
	private int age;

	public Person() {
		
	}

	public Person(int age) {
		this.age = age;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	@Override
	public int compareTo(Person e) {
		return age - e.age;
	}

	@Override
	public String toString() {
		return "age=" + age;
	}
}
public class BSTreeTest {
    @Test
    public void comparatorTest() {
        BSTree<Person> personBSTree = getPersonBSTree();
        BinaryTrees.println(personBSTree, BinaryTrees.PrintStyle.INORDER);
        BinaryTrees.println(personBSTree, BinaryTrees.PrintStyle.LEVEL_ORDER);
    }

    @Test
    public void traversalTest() {
        BSTree<Person> personBSTree = getPersonBSTree();
        BinaryTrees.println(personBSTree, BinaryTrees.PrintStyle.LEVEL_ORDER);
        personBSTree.preorderTraversal(new BinaryTree.Visitor<Person>() {
            @Override
            public boolean visit(Person element) {
//                element.setHeight(element.getHeight() + 1);
                System.out.println(element.getHeight());
                if (element.getHeight() == 2.05) {
                    return true;
                }
                return false;
            }
        });
    }

    @Test
    public void preorderTraversalPrintTest() {
        BSTree<Person> personBSTree = getPersonBSTree();
        System.out.println(personBSTree.toString());
    }

    private BSTree<Person> getPersonBSTree() {
        BSTree<Person> personBSTree = new BSTree<>(new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge().compareTo(o2.getAge());
            }
        });
        personBSTree.add(new Person("张三",20,1.85));
        personBSTree.add(new Person("李四",16,2.05));
        personBSTree.add(new Person("王二",77,1.54));
        personBSTree.add(new Person("莫言",64,1.99));
        return personBSTree;
    }

    private class Person {
        private String name;
        private Integer age;
        private Double height;

        public Person() {

        }

        public Person(String name, Integer age, Double height) {
            this.name = name;
            this.age = age;
            this.height = height;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Integer getAge() {
            return age;
        }

        public void setAge(Integer age) {
            this.age = age;
        }

        public Double getHeight() {
            return height;
        }

        public void setHeight(Double height) {
            this.height = height;
        }

        @Override
        public String toString() {
            return age.toString();
        }
    }
}

4. AVL树

4.1 理解

平衡因子(Balance Factor):某结点的左右子树的高度差(左 - 右)

AVL树的特点:

  • 每个节点的平衡因子只可能是 1、0、-1(绝对值 ≤ 1,如果超过 1,称之为“失衡”)

  • 每个节点的左右子树高度差不超过 1

  • 搜索、添加、删除的时间复杂度是 O(logn)

说明:红黑树的添加删除后的旋转恢复平衡都是O(1)级别的。AVL树添加后的旋转恢复平衡是O(1)级别的,而删除后的旋转恢复平衡操作的最坏情况达到了O(logn)级别

clipboard

注意:

① AVL树是最早发明的自平衡二叉搜索树之一

② AVL 取名于两位发明者的名字 :G. M. Adelson-Velsky 和 E. M. Landis(来自苏联的科学家)

4.2 继承机构

clipboard (1)

4.3 添加导致的失衡

示例:往下面这棵子树中添加 13

  • 最坏情况:可能会导致所有祖先节点都失衡

  • 父节点、非祖先节点,都不可能失衡

clipboard (2)

四种添加失衡情况及其处理(有且仅有四种)

  • LL-g右旋转(单旋)
clipboard (3)
  • RR-g左旋转(单旋)
clipboard (4)
  • LR-p左旋转,g右旋转(双旋)
clipboard (5)
  • RL-p右旋转,g左旋转(双旋)
clipboard (6)

四种添加失衡情况的统一处理

clipboard (7)

4.4 删除导致的失衡

示例:删除下面这棵树的16

clipboard (8)

删除后需要使用 LL-右旋转 解决失衡的情况

  • 如果绿色节点不存在,更高层的祖先节点可能也会失衡,需要再次恢复平衡,然后又可能导致更高层的祖先节点失衡...
  • 极端情况下,所有祖先节点都需要进行恢复平衡的操作,共 O(logn) 次调整
clipboard

删除后需要使用 RR-左旋转 解决失衡的情况

clipboard (1)

删除后需要使用 LR-p左旋转,g右旋转(双旋) 解决失衡的情况

clipboard (2)

删除后需要使用 RL-p右旋转,g左旋转(双旋) 解决失衡的情况

clipboard (3)

4.5 总结

添加

  • 可能会导致 所有祖先节点 都失衡

  • 只要让高度最低的失衡节点恢复平衡,整棵树就恢复平衡【仅需 O(1) 次调整

删除

  • 可能会导致父节点或祖先节点失衡(只有1个节点会失衡

  • 恢复平衡后,可能会导致更高层的祖先节点失衡【最多需要 O(logn) 次调整

平均时间复杂度

  • 搜索:O(logn)

  • 添加:O(logn),仅需 O(1) 次的旋转操作

  • 删除:O(logn),最多需要 O(logn) 次的旋转操作

4.6 代码实现

平衡二叉搜索树

/**
 * @Description 平衡二叉搜索树
 * @author Polaris
 * @version
 * @date 2020年3月10日下午8:33:51
 */
public class BBSTree<E> extends BSTree<E>{
	
	public BBSTree() {
		this(null);
	}

	public BBSTree(Comparator<E> comparator) {
		super(comparator);
	}
	
	/**
	 *	左旋转,以RR为例
	 */
	protected void rotateLeft(Node<E> grand) {
		Node<E> parent = grand.right;
		Node<E> child = parent.left;//child就是T1子树
		grand.right = child;
		parent.left = grand;
		
		afterRotate(grand, parent, child);
	}
	
	/**
	 *	右旋转,以LL为例
	 */
	protected void rotateRight(Node<E> grand) {
		Node<E> parent = grand.left;
		Node<E> child = parent.right;
		grand.left = child;
		parent.right = grand;
		
		afterRotate(grand, parent, child);
	}
	
	/**
	 * 	抽取左旋转和右旋转中的重复代码
	 */
	protected void afterRotate(Node<E> grand,Node<E> parent,Node<E> child) {
		//更新parent的parent(让parent成为子树的根节点)
		parent.parent = grand.parent;
		if(grand.isLeftChild()) {
			grand.parent.left = parent;
		} else if(grand.isRightChild()) {
			grand.parent.right = parent;
		} else { //grand是root节点
			root = parent;
		}
		//更新child的parent
		if(child != null) {
			child.parent = grand;		
		}
		//更新grand的parent
		grand.parent = parent;
	}
	
	/**
	 * 	统一旋转
	 */
	protected void rotate(
			Node<E> r, //之前的根节点
			Node<E> a,Node<E> b,Node<E> c,
			Node<E> d,
			Node<E> e,Node<E> f,Node<E> g) {
		//让d成为这棵子树的根节点
		d.parent = r.parent;
		if(r.isLeftChild()) {
			r.parent.left = d;
		} else if(r.isRightChild()) {
			r.parent.right = d;
		} else {
			root = d;
		}
		//处理a,b,c之间的关系
		b.left = a;
		if(a != null) {
			a.parent = b;
		}
		b.right = c;
		if(c != null) {
			c.parent = b;
		}
		
		//处理e,f,g之间的关系
		f.left = e;
		if(e != null) {
			e.parent = f;
		}
		f.right = g;
		if(g != null) {
			g.parent = f;
		}
		
		//处理b,d,f之间的关系
		d.left = b;
		d.right = f;
		b.parent = d;
		f.parent = d;
	}
	
}

AVL树

/**
 * @Description AVL树
 * @author Polaris
 * @version
 * @date 2020年3月10日下午8:35:05
 */
public class AVLTree<E> extends BBSTree<E> {
	
	public AVLTree() {
		this(null);
	}

	public AVLTree(Comparator<E> comparator) {
		super(comparator);
	}
	
	/**
	 * AVL树特有的节点,多了height属性用于计算平衡因子
	 */
	private static class AVLNode<E> extends Node<E> {
		//每个新添加的未经过处理的节点必然是叶子节点(高度默认为 1)
		int height = 1;//AVL树平衡因子:左子树高度 - 右子树高度(默认为叶子节点的高度1)
		
		public AVLNode(E element, Node<E> parent) {
			super(element, parent);
		}
		
		/*
		 * 	更新当前节点自己的高度
		 */
		public void updateHeight() {
			int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
			int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
			height = 1 + Math.max(leftHeight, rightHeight);
		}
		
		/*
		 * 	获取当前节点的平衡因子
		 */
		public int balanceFactor() {
			int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
			int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
			return leftHeight - rightHeight;
		}
		
		/*
		 * 	获取当前节点高度更高一点的子树
		 */
		public Node<E> tallerChild() {
			int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
			int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
			if(leftHeight > rightHeight) return left;
			if(leftHeight < rightHeight) return right;
			//当左右子树相等时,就返回和当前节点同方向(比如当前节点parent的左子树)的子树
			return isLeftChild() ? left :right;
		}
		
		@Override
		public String toString() {
			String parentString = "null";
			if (parent != null) {
				parentString = parent.element.toString();
			}
			return element + "_p(" + parentString + ")_h(" + height + ")";
		}
	}
	
	/**
	 * 	重写createNode,用于创建AVL特有的AVL节点
	 */
	@Override
	protected Node<E> createNode(E element, Node<E> parent) {
		return new AVLNode<>(element, parent);
	}
	
	/**
	 * 	实现添加新节点后的处理操作(通过当前节点找到失衡节点进行调整)
	 */
	@Override
	protected void afterAdd(Node<E> node) {
		while ((node = node.parent) != null) {
			if (isBalanced(node)) {	//如果平衡
				//更新高度(如果采用递归更新高度效率太差,直接在找parent失衡节点时就更新高度)
				updateHeight(node);
			} else {	//如果不平衡(记得要在恢复平衡时更新高度)
				// 恢复平衡
				rebalance(node);
				break;//找到一个不平衡节点恢复平衡则整棵树都平衡
			}
		}
	}
	
	@Override
	protected void afterRemove(Node<E> node,Node<E> replacement) {
		while ((node = node.parent) != null) {
			if (isBalanced(node)) {	//如果平衡
				//更新高度(如果采用递归更新高度效率太差,直接在找parent失衡节点时就更新高度)
				updateHeight(node);
			} else {	//如果不平衡(记得要在恢复平衡时更新高度)
				// 恢复平衡
				rebalance(node);
			}
		}
	}
	
	/**
	 * 	判断当前节点是否平衡
	 */
	private boolean isBalanced(Node<E> node) {
		return Math.abs(((AVLNode<E>)node).balanceFactor()) <= 1;
	}
	
	/**
	 * 	更新某个节点的高度(将强制转换封装为方法)
	 */
	private	void updateHeight(Node<E> node) {
		((AVLNode<E>)node).updateHeight();
	}
	
	//————————方式一:分开处理——————————
	/**
	 * 	恢复平衡(四种失衡情况单独处理)
	 * @param node 高度最低的那个不平衡节点
	 */
	private void rebalance(Node<E> grand) {
		//p是g左右子树中高度较高的子树
		Node<E> parent = ((AVLNode<E>)grand).tallerChild();
		//n是p左右子树中高度较高的子树
		Node<E> node = ((AVLNode<E>)parent).tallerChild();
		if(parent.isLeftChild()) { //L
			if(node.isLeftChild()) { //LL
				rotateRight(grand);//g左旋转
			} else { //LR
				rotateLeft(parent);//p左旋转
				rotateRight(grand);//g右旋转
			}
		} else { //R
			if(node.isLeftChild()) { //RL
				rotateRight(parent);//p右旋转
				rotateLeft(grand);//g左旋转
			} else { //RR
				rotateLeft(grand);
			}
		}
	}
	
	@Override
	protected void afterRotate(Node<E> grand, Node<E> parent, Node<E> child) {
		super.afterRotate(grand, parent, child);
		//更新高度
		updateHeight(grand);//g比较矮
		updateHeight(parent);//p比较高
	}
	
	//————————方式二:统一处理————————
	/**
	 * 	恢复平衡(四种失衡情况一起处理)
	 * @param node 高度最低的那个不平衡节点
	 */
	private void rebalance2(Node<E> grand) {
		//p是g左右子树中高度较高的子树
		Node<E> parent = ((AVLNode<E>)grand).tallerChild();
		//n是p左右子树中高度较高的子树
		Node<E> node = ((AVLNode<E>)parent).tallerChild();
		if(parent.isLeftChild()) { //L
			if(node.isLeftChild()) { //LL
				rotate(grand,node.left,node,node.right, 
						parent,parent.right,grand,grand.right);
			} else { //LR
				rotate(grand,parent.left,parent,node.left,
						node,node.right,grand,grand.right);
			}
		} else { //R
			if(node.isLeftChild()) { //RL
				rotate(grand,grand.left,grand,node.left,
						node,node.right,parent,parent.right);
			} else { //RR
				rotate(grand,grand.left,grand,parent.left,
						parent,node.left,node,node.right);
			}
		}
	}
	
	@Override
	protected void rotate(Node<E> r, Node<E> a, Node<E> b, Node<E> c, Node<E> d, Node<E> e, Node<E> f, Node<E> g) {
		super.rotate(r, a, b, c, d, e, f, g);
		//更新高度
		updateHeight(b);
		updateHeight(f);
		updateHeight(d);
	}
}

4.7 测试

public class AVLTreeTest {
	//添加删除测试
	@Test
	public void test() {
		Integer[] data = new Integer[] {
				67,52,92,96,53,95,13,63,34,82,76,54,9,68,39};
		AVLTree<Integer> avl = new AVLTree<>();
		for (int i = 0; i < data.length; i++) {
			avl.add(data[i]);
		}
		BinaryTrees.println(avl);
		for (int i = 0; i < data.length; i++) {
			avl.remove(data[i]);
			System.out.println("----------------------------");
			System.out.println("【" + data[i] + "】");
			BinaryTrees.println(avl);
		}
	}
}

5. B树

5.1 理解

B树 是一种 平衡的多路搜索树,多用于文件系统、数据库的实现

clipboard (4)

仔细观察B树,有什么眼前一亮的特点?

  • 1 个节点可以存储超过 2 个元素、可以拥有超过 2 个子节点

  • 拥有二叉搜索树的一些性质

  • 平衡,每个节点的所有子树高度一致

  • 比较矮

数据库中一般使用 200 ~ 300 阶B树

5.2 m阶B树的性质

规定的B树必须要遵守的一些性质

假设一个节点存储的元素个数为 x

  • 根节点:1 ≤ x ≤ m − 1

  • 非根节点:┌ m/2 ┐ − 1 ≤ x ≤ m − 1 (┌ ┐ => 向上取整)

  • 如果有子节点,子节点个数 :y = x + 1

    • 根节点:2 ≤ y ≤ m

    • 非根节点:┌ m/2 ┐ ≤ y ≤ m

      ➢ 比如 m = 3,2 ≤ y ≤ 3,因此可以称为(2, 3)树、2-3树
      ➢ 比如 m = 4,2 ≤ y ≤ 4,因此可以称为(2, 4)树、2-3-4树
      ➢ 比如 m = 5,3 ≤ y ≤ 5,因此可以称为(3, 5)树
      ➢ 比如 m = 6,3 ≤ y ≤ 6,因此可以称为(3, 6)树
      ➢ 比如 m = 7,4 ≤ y ≤ 7,因此可以称为(4, 7)树

5.3 B树 与二叉搜索树 的关系

B树 和 二叉搜索树,在逻辑上是等价的

多代节点合并,可以获得一个 超级节点

  • 2代合并的超级节点,最多拥有 4 个子节点(至少是 4阶B树)

  • 3代合并的超级节点,最多拥有 8 个子节点(至少是 8阶B树)

  • n代合并的超级节点,最多拥有 2^n 个子节点( 至少是 2^n 阶B树)

m阶B树,最多需要 log2m 代合并

clipboard (5)

5.4 B树搜索

跟二叉搜索树的搜索类似

  • 先在节点内部从小到大开始搜索元素

  • 如果命中,搜索结束

  • 如果未命中,再去对应的子节点中搜索元素,重复步骤 1

clipboard (6)

5.5 B树添加

新添加的元素必定是添加到 叶子节点 中 √ 红黑树会用到这个结论clipboard

  • 插入55
    clipboard (1)
  • 插入98
clipboard (2)
  • 再插入 98 呢?(假设这是一棵 4阶B树)

    • 最右下角的叶子节点的元素个数将超过限制
    • 这种现象可以称之为:上溢(overflow)

添加 – 上溢的解决(假设5阶)

  • 上溢节点的元素个数必然等于 m

  • 假设上溢节点最中间元素的位置为 k

    • 将 k 位置的元素向上与父节点合并

    • 将 [0, k-1] 和 [k + 1, m - 1] 位置的元素分裂成 2 个子节点

      • 这 2 个子节点的元素个数,必然都不会低于最低限制(┌ m/2 ┐ − 1)
  • 一次分裂完毕后,有可能导致父节点上溢,依然按照上述方法解决

    • 最极端的情况,有可能一直分裂到根节点。如果一直传播到根节点就会导致B树变高(仅此一种情况导致B树变高)
clipboard (3)

插入98

clipboard

插入52

clipboard (1)

插入54

clipboard (2)

5.6 B树删除

如果需要删除的元素在 叶子节点 中,那么直接删除即可

clipboard

如果需要删除的元素在 非叶子节点 中

  • 先找到前驱或后继元素,覆盖所需删除元素的值
  • 再把前驱或后继元素删除
clipboard (1)
  • 非叶子节点 的前驱或后继元素,必定在 叶子节点
    • 所以这里的删除前驱或后继元素 ,就是最开始提到的情况:删除的元素在叶子节点中
    • 真正的删除元素都是发生在叶子节点中 √红黑树会用到这个结论

删除-非叶子节点的 下溢 现象

  • 删除 22 ?(假设这是一棵 5阶B树)

    • 叶子节点被删掉一个元素后,元素个数可能会低于最低限制( 即┌ m/2 ┐−1 )
    • 这种现象称为:**下溢(underflow)**
clipboard (2)

删除-非叶子节点的 下溢 解决

  • 下溢节点的元素数量必然等于 **┌ m/2 ┐ − 2**

  • 如果下溢节点临近的兄弟节点,有至少 **┌ m/2 ┐** 个元素,可以向其借一个元素

    • 将父节点的元素 **b** 插入到下溢节点的 **0** 位置(最小位置)
    • 用兄弟节点的元素 **a**(最大的元素)替代父节点的元素 b
    • 这种操作其实就是:**旋转**

注意:因为 b > a,所以不能破环二叉搜索树的性质直接将a放到下溢节点去。

clipboard (5)
  • 如果下溢节点临近的兄弟节点,只有 **┌ m/2 ┐ − 1** 个元素
  • 将父节点的元素 b 挪下来跟左右子节点进行合并
  • 合并后的节点元素个数等于**┌ m/2 ┐ + ┌ m/2 ┐ − 2**,不会超过 **m − 1** 上溢
  • 这个操作可能会导致父节点下溢,依然按照上述方法解决,下溢现象可能会一直往上传播。如果一直传播到根节点就会导致B树变矮(仅此一种情况导致B树变矮)
clipboard (6)

5.7 理解4阶b树

"理解了4阶b树,将能更好的学习理解 红黑树"

4阶B树的性质

  • 所有节点能存储的元素个数 x :1 ≤ x ≤ 3
  • 所有非叶子节点的子节点个数 y :2 ≤ y ≤ 4

添加

  • 手绘 从 1 添加到 22

删除

  • 手绘 从 1 删除到 22

6. 红黑树

6.1 理解

红黑树也是一种 自平衡的二叉搜索树,以前也叫做平衡二叉B树(Symmetric Binary B-tree)。

**红黑树必须满足以下 5 条性质 **

  • 节点是 RED 或者 BLACK

  • 根节点是 BLACK

  • 叶子节点(外部节点,空节点)都是 BLACK

  • RED 节点的子节点都是 `BLACK

    • RED 节点的 parent 都是 BLACK

    • 从根节点到叶子节点的所有路径上不能有 2 个连续的 RED 节点

  • 从任一节点到叶子节点的所有路径都包含 相同数目BLACK 节点

clipboard

注意:红黑树的 叶子节点 是让原来度为 0 的节点或度为 1 的节点都变成度为 2 的节点后的叶子节点。(增加空节点 null 实现此功能)此时红黑树就变成了真二叉树。

clipboard (1)

注意:之后展示的红黑树都会省略 null 节点 (空节点是假想出来的)

红黑树的平衡 (为什么满足以上5条性质,就能保证红黑树是平衡的?)

  • 以上5条性质,可以保证 红黑树 等价于 4阶B树

  • 相比AVL树,红黑树的平衡标准比较宽松:没有一条路径会大于其他路径的2倍

  • 可以理解为是一种弱平衡、黑高度平衡 (任意一条路的黑节点数量都是相等的)

  • 红黑树的最大高度是 2 ∗ log(n + 1) ,依然是 O(logn) 级别

红黑树的平均时间复杂度

  • 搜索:O(logn)

  • 添加:O(logn),O(1) 次的旋转操作

  • 删除:O(logn),O(1) 次的旋转操作

AVL树 对比 红黑树

  • AVL树

    • 平衡标准比较严格:每个左右子树的高度差不超过1

    • 最大高度是 1.44 ∗ log(n + 2) − 1.328(100W个节点,AVL树最大树高28)

    • 搜索、添加、删除都是 O(logn) 复杂度,其中添加仅需 O(1) 次旋转调整、删除最多需要 O(logn) 次旋转调整

  • 红黑树

    • 平衡标准比较宽松:没有一条路径会大于其他路径的2倍

    • 最大高度是 2 ∗ log(n + 1)( 100W个节点,红黑树最大树高40)

    • 搜索、添加、删除都是 O(logn) 复杂度,其中添加、删除都仅需 O(1) 次旋转调整

  • 搜索的次数远远大于插入和删除,选择AVL树;搜索、插入、删除次数几乎差不多,选择红黑树

  • 相对于AVL树来说,红黑树牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树

  • 红黑树的平均统计性能优于AVL树,实际应用中更多选择使用红黑树

6.2 红黑树的等价变换

红黑树 和 四阶B树(2-3-4树)具有等价性

BLACK 节点与它的 RED 子节点融合在一起,形成1个B树节点

红黑树的 BLACK 节点个数 与 4阶B树的节点总个数 相等

注意:用 2-3树 与 红黑树 进行类比,这是极其不严谨的,2-3树 并不能完美匹配 红 黑树 的所有情况

clipboard (2)

6.3 红黑树 与 2-3-4树 的比较

如果下图最底层的 BLACK 节点是不存在的,在B树中是什么样的情形?=>整棵B树只有1个节点,而且是超级节点

clipboard (3)

6.4 添加节点

已知:

  • B树中,新元素必定是添加到叶子节点中

  • 4阶B树所有节点的元素个数 x 都符合 1 ≤ x ≤ 3

注意:

① 建议新添加的节点默认为 RED,这样能够让红黑树的性质尽快满足(性质1,2,3,5 都满足,性质 4 不一定)

② 如果添加的是根节点,染成 BLACK 即可

添加的所有情况

clipboard (4)

有 4 种情况既满足红黑树的性质四:parent 为 BLACK,同时也满足4阶B树的性质,因此不用做任何额外的处理。

clipboard (5)

有 8 种情况不满足红黑树的性质四:parent 为 RED( Double Red ),其中前 4 种属于B树节点上溢的情况

clipboard (6)

添加 – 修复性质4 – LL\RR

判定条件:uncle 不是 RED

  • parent 染成 BLACK,grand 染成 RED

  • grand 进行单旋操作:

    • LL:右旋转
    • RR:左旋转
clipboard

添加 – 修复性质4 – LR\RL

判定条件:uncle 不是 RED

  • 自己染成 BLACK,grand 染成 RED

  • 进行双旋操作:

    • LR:parent 左旋转, grand 右旋转
    • RL:parent 右旋转, grand 左旋转
clipboard (1)

添加 – 修复性质4 – 上溢 – LL

注意:之前修复的四种情况,添加节点的叔父节点都为null(null默认记为黑色)。

判定条件:uncle 是 RED

  • parent、uncle 染成 BLACK
  • grand 向上合并,且染成 RED,当做是新添加的节点进行处理

grand 向上合并时,可能继续发生上溢

若上溢持续到根节点,只需将根节点染成 BLACK

clipboard (2)

添加 – 修复性质4 – 上溢 – RR

判定条件:uncle 是 RED

  • parent、uncle 染成 BLACK
  • grand 向上合并,且染成 RED,当做是新添加的节点进行处理
clipboard (3)

添加 – 修复性质4 – 上溢 – LR

判定条件:uncle 是 RED

  • parent、uncle 染成 BLACK
  • grand 向上合并,且染成 RED,当做是新添加的节点进行处理
clipboard (4)

添加 – 修复性质4 – 上溢 – RL

判定条件:uncle 是 RED

  • parent、uncle 染成 BLACK
  • grand 向上合并,且染成 RED,当做是新添加的节点进行处理
clipboard (5)

6.4 删除节点

已知:B树中,最后真正被删除的元素都在叶子节点中

删除-RED节点

直接删除,不用做任何调整

clipboard

删除 – BLACK 节点 (有 3 种情况)

删除拥有 2 个 RED 子节点的 BLACK 节点 (如 25)

  • 不可能被直接删除,因为会找它的前驱节点或后继节点替代删除,在BSTree中已经实现了此功能因此也不用考虑这种情况

删除拥有 1 个 RED 子节点的 BLACK 节点 (如 46,76)

删除 BLACK 叶子节点 (如 88)

总结:删除后真正需要处理的只有两种情况:① 删除拥有 1 个 RED 子节点的 BLACK 节点 ② 删除 BLACK 叶子节点

删除 - 拥有 1 个 RED 子节点的 BLACK 节点

判定条件:用以替代的子节点是 RED “注意:删除Black叶子节点,没有用于替代的就相当于用null(默认为Black)替代”

将替代的子节点染成 BLACK 即可保持红黑树性质

clipboard (1)

删除 - BLACK 叶子节点 - sibling为 BLACK

BLACK 叶子节点被删除后,会导致B树节点下溢(比如删除88)

判定条件:如果 sibling 至少有 1 个 RED 子节点

  • 进行旋转操作
  • 旋转之后的中心节点继承 parent 的颜色
  • 旋转之后的左右节点染为 BLACK
clipboard (2)

判定条件:如果 sibling 没有 RED 子节点

  • 将 sibling 染成 RED、parent 染成 BLACK 即可修复红黑树性质 (合并)
  • 如果 parent 是 BLACK 会导致 parent 也下溢,这时只需要把 parent 当做被删除的节点处理即可(递归)
clipboard (3)

删除 - BLACK 叶子节点 - sibling为 RED

如果 sibling 是 RED

  • sibling 染成 BLACK,parent 染成 RED,进行旋转
  • 于是又回到 sibling 是 *BLACK 的情况
clipboard (4)

6.5 实现

/**
 * @Description 红黑树
 * @author Polaris
 * @version
 * @date 2020年3月10日下午8:35:16
 */
public class RBTree<E> extends BBSTree<E>{
	private static final boolean RED = false;
	private static final boolean BLACK = true;
	
	public RBTree() {
		this(null);
	}

	public RBTree(Comparator<E> comparator) {
		super(comparator);
	}
	
	/**
	 * RB树特有的节点
	 */
	private static class RBNode<E> extends Node<E> {
		boolean color = RED;
		public RBNode(E element, Node<E> parent) {
			super(element, parent);
		}
		
		@Override
		public String toString() {
			String str = "";
			if(color == RED) {
				str = "R_";
			}
			return str + element.toString();
		}
	}
	
	@Override
	protected Node<E> createNode(E element, Node<E> parent) {
		return new RBNode<E>(element,parent);
	}
	
	/**
	 * 给节点上色
	 */
	private Node<E> color(Node<E> node,boolean color) {
		if(node == null) return node;
		((RBNode<E>)node).color = color;
		return node;
	}
	
	/**
	 * 将节点染成红色
	 */
	private Node<E> red(Node<E> node){
		return color(node,RED);
	}
	
	/**
	 * 将节点染成黑色
	 */
	private Node<E> black(Node<E> node){
		return color(node,BLACK);
	}
	
	/**
	 * 获取当前节点的颜色
	 */
	private boolean colorOf(Node<E> node) {
		return node == null ? BLACK : ((RBNode<E>)node).color;
	}
	
	/**
	 * 判断当前颜色是否为黑色
	 */
	private boolean isBlack(Node<E> node) {
		return colorOf(node) == BLACK;
	}
	
	/**
	 * 判断当前颜色是否为红色
	 */
	private boolean isRed(Node<E> node) {
		return colorOf(node) == RED;
	}

	/**
	 * 	实现添加新节点后的处理操作
	 */
	@Override
	protected void afterAdd(Node<E> node) {
		Node<E> parent = node.parent;
		//添加的是根节点 或 上溢到根节点
		if(parent == null) {
			black(node);
			return;
		}
		//类型一:parent是黑色(不用处理四种情况)
		if(isBlack(parent)) return;
		//类型二:parent是红色且uncle是红色(会上溢的四种情况)
		Node<E> uncle = parent.getSibling();
		Node<E> grand = red(parent.parent);//以下情况都需要将grand染成红色,可以统一处理
		if(isRed(uncle)) {
			black(parent);
			black(uncle);
			//把祖父节点当作是新添加的节点
			afterAdd(grand);//上溢递归调用
			return;
		}
		//类型三:parent是红色且uncle不是红色(需要旋转的四种情况)
		if(parent.isLeftChild()) {//L
			if(node.isLeftChild()) { //LL
				black(parent);
			} else { //LR
				black(node);
				rotateLeft(parent);
			}
			rotateRight(grand);
		} else { //R
			if(node.isLeftChild()) { //RL
				black(node);
				rotateRight(parent);
			} else { //RR
				black(parent);
			}
			rotateLeft(grand);
		}
	}
	
	/**
	 * 	实现删除节点后的处理操作
	 */
	@Override
	protected void afterRemove(Node<E> node,Node<E> replacement) {
		//情况一:如果删除的节点是红色,不用处理
		if(isRed(node)) return;
		//情况二:用于取代node子节点的是红色节点
		if(isRed(replacement)) {
			black(replacement);
			return;
		}
		//情况三:删除的是黑色叶子节点(下溢)
		Node<E> parent = node.parent;
		//删除的是根节点
		if(parent == null) return;
		//判断被删除的node的节点是左还是右
		boolean left = parent.left == null || node.isLeftChild();
		Node<E> sibling = left ? parent.right : parent.left;
		if(left) { //被删除的节点在左边,兄弟节点在右边(镜像对称处理)
			if(isRed(sibling)) { //兄弟节点是红色,就要转成黑色
				black(sibling);
				red(parent);
				rotateLeft(parent);
				//更换兄弟
				sibling = parent.right;
			}
			//兄弟节点必然是黑色
			if(isBlack(sibling.left) && isBlack(sibling.right)) {
				//兄弟节点没有一个红色子节点,父节点要向下向子节点合并
				boolean parentBlack = isBlack(parent);
				black(parent);
				red(sibling);
				if(parentBlack) {
					afterRemove(parent, null);
				}
			} else { //兄弟节点至少有 1 个红色节点,就要向兄弟节点借元素
				if(isBlack(sibling.right)) {
					//兄弟节点的右边不是红色,则兄弟要先旋转
					rotateRight(sibling);
					sibling = parent.right;
				}
				color(sibling,colorOf(parent));
				black(sibling.right);
				black(parent);
				rotateLeft(parent);
			}
		} else { //被删除的节点在右边,兄弟节点在左边(图示的是这种)
			if(isRed(sibling)) { //兄弟节点是红色,就要转成黑色
				black(sibling);
				red(parent);
				rotateRight(parent);
				//更换兄弟
				sibling = parent.left;
			}
			//兄弟节点必然是黑色
			if(isBlack(sibling.left) && isBlack(sibling.right)) {
				//兄弟节点没有一个红色子节点,父节点要向下向子节点合并
				boolean parentBlack = isBlack(parent);
				black(parent);
				red(sibling);
				if(parentBlack) {
					afterRemove(parent, null);
				}
			} else { //兄弟节点至少有 1 个红色节点,就要向兄弟节点借元素
				if(isBlack(sibling.left)) {
					//兄弟节点的左边不是红色,则兄弟要先旋转
					rotateLeft(sibling);
					sibling = parent.left;
				}
				color(sibling,colorOf(parent));
				black(sibling.left);
				black(parent);
				rotateRight(parent);
			}
		}
	}
	
}

6.6 测试

public class RBTreeTest {
	//添加测试
	@Test
	public void test() {
		Integer[] data = new Integer[] {
				55,87,56,74,96,22,62,20,70,68,90,50};
		RBTree<Integer> rb = new RBTree<>();
		for (int i = 0; i < data.length; i++) {
			rb.add(data[i]);
			System.out.println("----------------------------");
			System.out.println("【" + data[i] + "】");
			BinaryTrees.println(rb);
		}
		BinaryTrees.println(rb);
	}
	
	//删除测试
	@Test
	public void test1() {
		Integer[] data = new Integer[] {
				55,87,56,74,96,22,62,20,70,68,90,50};
		RBTree<Integer> rb = new RBTree<>();
		for (int i = 0; i < data.length; i++) {
			rb.add(data[i]);
		}
		BinaryTrees.println(rb);
		for (int i = 0; i < data.length; i++) {
			rb.remove(data[i]);
			System.out.println("----------------------------");
			System.out.println("【" + data[i] + "】");
			BinaryTrees.println(rb);
		}
	}
}

六. 集合(Set)实现

1. 集合的特点

不存放重复的元素

常用于去重

  • 存放新增 IP,统计新增 IP 量

  • 存放词汇,统计词汇量

集合的内部实现能直接使用 动态数组链表二叉搜索树(AVL树,红黑树) 实现

2. 接口设计

/**
 * @Description 集合Set的接口
 * @author Polaris
 * @version
 * @date 2020年3月11日下午9:30:36
 */
public interface Set<E> {
	int size();
	boolean isEmpty();
	void clear();
	boolean contains(E element);
	void add(E element);
	void remove(E element);
	void traversal(Visitor<E> visitor);
	
	/**
	 * 注意:动态数组或链表有索引的概念能直接for循环遍历,不需要遍历接口
	 */
	public static abstract class Visitor<E> {
		boolean stop;
		public abstract boolean visit(E element);
	}
}

3. 链表实现集合(ListSet)

3.1 实现

public class ListSet<E> implements Set<E> {
	private List<E> list = new LinkedList<E>();
	
	@Override
	public int size() {
		return list.size();
	}

	@Override
	public boolean isEmpty() {
		return list.isEmpty();
	}

	@Override
	public void clear() {
		list.clear();
	}

	@Override
	public boolean contains(E element) {
		return list.contains(element);
	}

	@Override
	public void add(E element) {
		//集合Set存储不重复的元素
		if(list.contains(element)) return;
		list.add(element);
	}

	@Override
	public void remove(E element) {
		int index = list.indexOf(element);
		if(index != -1) {
			list.remove(index);
		}
	}

	@Override
	public void traversal(Visitor<E> visitor) {
		if(visitor == null) return;
		int size = list.size();
		for(int i = 0;i < size;i++) {
			if(visitor.visit(list.get(i))) return;
		}
	}

}

3.2 测试

public class ListSetTest {
	@Test
	public void test() {
		Set<Integer> listSet = new ListSet<>();
		listSet.add(10);
		listSet.add(11);
		listSet.add(11);
		listSet.add(12);
		listSet.add(7);
		listSet.remove(11);
		listSet.traversal(new Visitor<Integer>() {
			@Override
			public boolean visit(Integer element) {
				System.out.println(element);
				return false;
			}
		});

	}
}

4. 红黑树实现集合(TreeSet)

4.1 ListSet 与 TreeSet效率对比

链表

  • 查找:最坏情况为O(n)级别

  • 添加:最坏情况为O(n)级别

  • 删除:最坏情况为O(n)级别

红黑树

  • 查找:最坏情况为O(logn)级别

  • 添加:最坏情况为O(logn)级别

  • 删除:最坏情况为O(logn)级别

4.2 TreeSet 的局限性

通过二叉搜索树实现的TreeSet,元素必须具备 可比较性 才能加进去

通过 哈希表 实现的 HashSet,可以解决这个局限性

4.3 实现

public class TreeSet<E> implements Set<E>{
	private RBTree<E> tree;
	
	public TreeSet() {
		this(null);
	}
	
	public TreeSet(Comparator<E> comparator) {
		tree = new RBTree<>(comparator);
	}
	
	@Override
	public int size() {
		return tree.size();
	}

	@Override
	public boolean isEmpty() {
		return tree.isEmpty();
	}

	@Override
	public void clear() {
		tree.clear();
	}

	@Override
	public boolean contains(E element) {
		return tree.contains(element);
	}

	@Override
	public void add(E element) {
		tree.add(element);
	}

	@Override
	public void remove(E element) {
		tree.remove(element);
	}

	@Override
	public void traversal(Visitor<E> visitor) {
		tree.inorderTraversal(new BinaryTree.Visitor<E>() {
			@Override
			public boolean visit(E element) {
				return visitor.visit(element);
			}
			
		});
	}

}

4.4 测试

public class TreeSetTest {
	@Test
	public void test() {
		Set<Person> treeSet = new TreeSet<>(new Comparator<Person>() {
			@Override
			public int compare(Person o1, Person o2) {
				Person p1 = (Person)o1;
				Person p2 = (Person)o2;
				return p1.getAge() - p2.getAge();
			}
		});
		treeSet.add(new Person(10));
		treeSet.add(new Person(12));
		treeSet.add(new Person(10));
		treeSet.add(new Person(7));
		treeSet.traversal(new Visitor<Person>() {
			@Override
			public boolean visit(Person element) {
				System.out.println(element.getAge());
				return false;
			}
		});

	}
}

class Person {
	private int age;

	public Person(int age) {
		super();
		this.age = age;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
	
}

七.映射(Map)实现

1. 理解

Map 在有些编程语言中也叫做字典(dictionary,比如 Python、Objective-C、Swift 等)

clipboard (5)

Map 的每一个 key 是唯一的

类似Set,Map可以直接利用链表二叉搜索树 (AVL树,红黑树)等数据结构来实现

2. Map 与 Set 的关系

Map 的所有 key 组合在一起,其实就是一个 Set。因此,Set 可以间接利用 Map 来作内部实现

3. 接口设计

public interface Map<K, V> {
	int size();
	boolean isEmpty();
	void clear();
	V put(K key,V value);
	V get(K key);
	V remove(K key);
	boolean containsKey(K key);
	boolean containsValue(V value);
	void traversal(Visitor<K,V> visitor);
	
	public static abstract class Visitor<K,V> {
		boolean stop;
		public abstract boolean visit(K key,V value);
	}
}

4. 红黑树实现TreeMap

4.1 TreeMap分析

时间复杂度(平均)

  • 添加、删除、搜索:O(logn)

特点

  • Key 必须具备可比较性

  • 元素的分布是有顺序的

在实际应用中,很多时候的需求

  • Map 中存储的元素不需要讲究顺序

  • Map 中的 Key 不需要具备可比较性

不考虑顺序、不考虑 Key 的可比较性,Map 有更好的实现方案,平均时间复杂度可以达到 O(1),那就是采取哈希表来实现 Map

4.2 实现

/**
 * @Description 红黑树实现映射(把TreeMap本身当成一棵红黑树,
 * 			用key和value代替element,即从头开始用红黑树实现一个Map)
 * @author Polaris
 * @version
 * @date 2020年3月12日下午6:25:57
 */
@SuppressWarnings({"unchecked","unused"})
public class TreeMap<K,V> implements Map<K,V>{
	private static final boolean RED = false;
	private static final boolean BLACK = true;
	
	protected int size;
	protected Node<K,V> root;// 根节点
	
	protected Comparator<K> comparator;// 比较器定制排序

	public TreeMap() {
		this(null);
	}

	public TreeMap(Comparator<K> comparator) {
		this.comparator = comparator;
	}
		
	private static class Node<K,V> {
		K key;
		V value;
		boolean color = RED;
		Node<K,V> left; // 左子节点
		Node<K,V> right; // 右子节点
		Node<K,V> parent; // 父节点
		
		public Node(K key,V value, Node<K,V> parent) {
			this.key = key;
			this.value = value;
			this.parent = parent;
		}
		
		public boolean isLeaf() {
			return left == null && right == null;
		}
		
		public boolean hasTwoChildren() {
			return left != null && right != null;
		}
		
		public boolean isLeftChild() {
			return parent != null && this == parent.left;
		}
		
		public boolean isRightChild() {
			return parent != null && this == parent.right;
		}
		
		public Node<K,V> getSibling(){
			if(isLeftChild()) {
				return parent.right;
			}
			if(isRightChild()) {
				return parent.left;
			}
			return null;
		}
	}
	
	/**
	 * 	检查key是否为空
	 */
	protected void keyNoNullCheck(K key) {
		if (key == null) {
			throw new IllegalArgumentException("key must no be null!");
		}
	}
	
	@Override
	public int size() {
		return size;
	}

	@Override
	public boolean isEmpty() {
		return size == 0;
	}

	@Override
	public void clear() {
		root = null;
		size = 0;
	}

	@Override
	public V put(K key, V value) { //一般只要求key具有可比较性就
		keyNoNullCheck(key);
		//添加第一个节点(根节点)
		if (root == null) {
			root = new Node<>(key, value, null);
			size++;
			afterPut(root);//新添加节点之后的处理
			return null;
		}
		// 如果添加的不是第一个节点:
		// 1.找到待添加位置的父节点
		Node<K,V> parent = root;
		Node<K,V> node = root;
		int cmp = 0;
		while(node != null) {
			cmp = compare(key, node.key);
			parent = node;
			if (cmp > 0) {
				node = node.right;
			} else if (cmp < 0) {
				node = node.left;
			} else {
				node.key = key;//一般覆盖(不同对象可能有相同的比较参数)
				V oldValue = node.value;
				node.value = value;
				return oldValue;
			}
		}
		// 2.判断插入父节点的左子节点还是右子节点
		Node<K,V> newNode = new Node<>(key, value, parent);
		if (cmp > 0) {
			parent.right = newNode;
		} else {
			parent.left = newNode;
		}
		size++;
		afterPut(newNode);//新添加节点之后的处理
		return null;
	}
	
	private void afterPut(Node<K,V> node) {
		Node<K,V> parent = node.parent;
		//添加的是根节点 或 上溢到根节点
		if(parent == null) {
			black(node);
			return;
		}
		//类型一:parent是黑色(不用处理四种情况)
		if(isBlack(parent)) return;
		//类型二:parent是红色且uncle是红色(会上溢的四种情况)
		Node<K,V> uncle = parent.getSibling();
		Node<K,V> grand = red(parent.parent);//以下情况都需要将grand染成红色,可以统一处理
		if(isRed(uncle)) {
			black(parent);
			black(uncle);
			//把祖父节点当作是新添加的节点
			afterPut(grand);//上溢递归调用
			return;
		}
		//类型三:parent是红色且uncle不是红色(需要旋转的四种情况)
		if(parent.isLeftChild()) {//L
			if(node.isLeftChild()) { //LL
				black(parent);
			} else { //LR
				black(node);
				rotateLeft(parent);
			}
			rotateRight(grand);
		} else { //R
			if(node.isLeftChild()) { //RL
				black(node);
				rotateRight(parent);
			} else { //RR
				black(parent);
			}
			rotateLeft(grand);
		}
	}
	
	private int compare(K k1, K k2) {
		if (comparator != null) {
			return comparator.compare(k1, k2);
		}
		return ((Comparable<K>)k1).compareTo(k2);
	}
	
	/**
	 * 给节点上色
	 */
	private Node<K,V> color(Node<K,V> node,boolean color) {
		if(node == null) return node;
		node.color = color;
		return node;
	}
	
	/**
	 * 将节点染成红色
	 */
	private Node<K,V> red(Node<K,V> node){
		return color(node,RED);
	}
	
	/**
	 * 将节点染成黑色
	 */
	private Node<K,V> black(Node<K,V> node){
		return color(node,BLACK);
	}
	
	/**
	 * 获取当前节点的颜色
	 */
	private boolean colorOf(Node<K,V> node) {
		return node == null ? BLACK : node.color;
	}
	
	/**
	 * 判断当前颜色是否为黑色
	 */
	private boolean isBlack(Node<K,V> node) {
		return colorOf(node) == BLACK;
	}
	
	/**
	 * 判断当前颜色是否为红色
	 */
	private boolean isRed(Node<K,V> node) {
		return colorOf(node) == RED;
	}
	
	/**
	 *	左旋转,以RR为例
	 */
	private void rotateLeft(Node<K,V> grand) {
		Node<K,V> parent = grand.right;
		Node<K,V> child = parent.left;//child就是T1子树
		grand.right = child;
		parent.left = grand;
		
		afterRotate(grand, parent, child);
	}
	
	/**
	 *	右旋转,以LL为例
	 */
	private void rotateRight(Node<K,V> grand) {
		Node<K,V> parent = grand.left;
		Node<K,V> child = parent.right;
		grand.left = child;
		parent.right = grand;
		
		afterRotate(grand, parent, child);
	}
	
	/**
	 * 	抽取左旋转和右旋转中的重复代码
	 */
	private void afterRotate(Node<K,V> grand,Node<K,V> parent,Node<K,V> child) {
		//更新parent的parent(让parent成为子树的根节点)
		parent.parent = grand.parent;
		if(grand.isLeftChild()) {
			grand.parent.left = parent;
		} else if(grand.isRightChild()) {
			grand.parent.right = parent;
		} else { //grand是root节点
			root = parent;
		}
		//更新child的parent
		if(child != null) {
			child.parent = grand;		
		}
		//更新grand的parent
		grand.parent = parent;
	}

	@Override
	public V get(K key) {
		Node<K,V> node = node(key);
		return node != null ? node.value : null;
	}
	
	/**
	 * 根据key找到对应节点
	 */
	private Node<K,V> node(K key){
		Node<K,V> node = root;
		while(node != null) {
			int cmp = compare(key,node.key);
			if(cmp == 0) return node;
			if(cmp > 0) {
				node = node.right;
			} else {
				node = node.left;
			}
		}
		return null;
	}

	@Override
	public V remove(K key) {
		return remove(node(key));
	}
	
	/**
	 * 根据key删除节点元素
	 */
	private V remove(Node<K,V> node) {
		if(node == null) return null;
		size--;
		V oldValue = node.value;
		//考虑度为2的节点,转化为度为1
		if(node.hasTwoChildren()) {
			Node<K,V> s = successor(node);//后继节点
			//用后继节点的值覆盖度为2的节点的值
			node.key = s.key;
			node.value = s.value;
			//删除后继节点
			node = s;
		}
		//删除node节点(能到这则说明node的度必为0或1)
		Node<K,V> replacement = node.left != null ? node.left : node.right;
		if(replacement != null) { //node是度为1的节点
			//更改parent
			replacement.parent = node.parent;
			//更改parent的left,right指向
			if(node.parent == null) { //node是度为1的节点也是根节点
				root = replacement;
			} else if(node == node.parent.left) {
				node.parent.left = replacement;
			} else { //在右边
				node.parent.right = replacement;
			}
			//此时开始恢复平衡(AVL树 或 RB树需要实现此方法)
			afterRemove(node,replacement);
		} else if(node.parent == null){ //node是叶子节点也是根节点
			root = null;
			afterRemove(node,null);
		} else { //node是叶子节点但不是根节点
			if(node == node.parent.left) {
				node.parent.left = null;
			} else {
				node.parent.right = null;
			}
			//此时开始恢复平衡(AVL树 或RB树 需要实现此方法)
			afterRemove(node,null);
		}
		return oldValue;
	}
	
	/**
	 * 	实现删除节点后的处理操作
	 */
	private void afterRemove(Node<K,V> node,Node<K,V> replacement) {
		//情况一:如果删除的节点是红色,不用处理
		if(isRed(node)) return;
		//情况二:用于取代node子节点的是红色节点
		if(isRed(replacement)) {
			black(replacement);
			return;
		}
		//情况三:删除的是黑色叶子节点(下溢)
		Node<K,V> parent = node.parent;
		//删除的是根节点
		if(parent == null) return;
		//判断被删除的node的节点是左还是右
		boolean left = parent.left == null || node.isLeftChild();
		Node<K,V> sibling = left ? parent.right : parent.left;
		if(left) { //被删除的节点在左边,兄弟节点在右边(镜像对称处理)
			if(isRed(sibling)) { //兄弟节点是红色,就要转成黑色
				black(sibling);
				red(parent);
				rotateLeft(parent);
				//更换兄弟
				sibling = parent.right;
			}
			//兄弟节点必然是黑色
			if(isBlack(sibling.left) && isBlack(sibling.right)) {
				//兄弟节点没有一个红色子节点,父节点要向下向子节点合并
				boolean parentBlack = isBlack(parent);
				black(parent);
				red(sibling);
				if(parentBlack) {
					afterRemove(parent, null);
				}
			} else { //兄弟节点至少有 1 个红色节点,就要向兄弟节点借元素
				if(isBlack(sibling.right)) {
					//兄弟节点的右边不是红色,则兄弟要先旋转
					rotateRight(sibling);
					sibling = parent.right;
				}
				color(sibling,colorOf(parent));
				black(sibling.right);
				black(parent);
				rotateLeft(parent);
			}
		} else { //被删除的节点在右边,兄弟节点在左边(图示的是这种)
			if(isRed(sibling)) { //兄弟节点是红色,就要转成黑色
				black(sibling);
				red(parent);
				rotateRight(parent);
				//更换兄弟
				sibling = parent.left;
			}
			//兄弟节点必然是黑色
			if(isBlack(sibling.left) && isBlack(sibling.right)) {
				//兄弟节点没有一个红色子节点,父节点要向下向子节点合并
				boolean parentBlack = isBlack(parent);
				black(parent);
				red(sibling);
				if(parentBlack) {
					afterRemove(parent, null);
				}
			} else { //兄弟节点至少有 1 个红色节点,就要向兄弟节点借元素
				if(isBlack(sibling.left)) {
					//兄弟节点的左边不是红色,则兄弟要先旋转
					rotateLeft(sibling);
					sibling = parent.left;
				}
				color(sibling,colorOf(parent));
				black(sibling.left);
				black(parent);
				rotateRight(parent);
			}
		}
	}
	
	/**
	 * 	利用中序遍历求某个节点的前驱节点
	 */
	private Node<K,V> predecessor(Node<K,V> node) {
		if(node == null) return null;
		//前驱节点在左子树中:node.left.right.right...
		Node<K,V> p = node.left;
		if(p != null) {
			while(p.right != null) {
				p = p.right;
			}
			return p;
		}
		//从祖父节点中寻找前驱节点
		while(node.parent != null && node == node.parent.left) {
			node = node.parent;
		}
		//情况一:node.parent == null ↓
		//情况二:node == node.parent.right ↓
		return node.parent;
	}
	
	/**
	 * 	利用中序遍历求某个节点的后继节点
	 */
	private Node<K,V> successor(Node<K,V> node) {
		if(node == null) return null;
		//前驱节点在右子树中:node.right.left.left...
		Node<K,V> p = node.right;
		if(p != null) {
			while(p.left != null) {
				p = p.left;
			}
			return p;
		}
		//从祖父节点中寻找前驱节点
		while(node.parent != null && node == node.parent.right) {
			node = node.parent;
		}
		//情况一:node.parent == null ↓
		//情况二:node == node.parent.left ↓
		return node.parent;
	}

	@Override
	public boolean containsKey(K key) {
		return node(key) != null;
	}

	@Override
	public boolean containsValue(V value) {
		if(root == null) return false;
		Queue<Node<K,V>> queue = new LinkedList<>();
		queue.offer(root);
		while(!queue.isEmpty()) {
			Node<K,V> node = queue.poll();
			if(valEquals(value, node.value)) return true;
			if(node.left != null) {
				queue.offer(node.left);
			}
			if(node.right != null) {
				queue.offer(node.right);
			}
		}
		return false;
	}
	
	private boolean valEquals(V v1,V v2) {
		return v1 == null ? v2 == null : v1.equals(v2);
	}

	@Override
	public void traversal(Visitor<K, V> visitor) {
		if(visitor == null) return;
		traversal(root,visitor);
	}
	
	private void traversal(Node<K,V> node,Visitor<K, V> visitor) {
		if(node == null || visitor.stop) return;
		traversal(node.left,visitor);
		if(visitor.stop) return;
		visitor.visit(node.key, node.value);
		traversal(node.right,visitor);
	}
	
}

4.3 测试

public class TreeMapTest {
	@Test
	public void test() {
		Map<String,Integer> map = new TreeMap<>(); 
		map.put("c", 2);
		map.put("a", 5);
		map.put("b", 6);
		map.put("a", 8);
		map.traversal(new Visitor<String, Integer>() {
			@Override
			public boolean visit(String key, Integer value) {
				System.out.println(key + "_" + value);
				return false;
			}
		});
	}
	
	@Test
	public void test2() {
		FileInfo fileInfo = Files.read("D:\\Learning\\Java"
				+ "\\workspace_eclipse\\workspace001_2019-3"
				+ "\\DataStructures\\src\\com\\polaris4"
				+ "\\map", 
				new String[]{"java"});
		
		System.out.println("文件数量:" + fileInfo.getFiles());
		System.out.println("代码行数:" + fileInfo.getLines());
		String[] words = fileInfo.words();
		System.out.println("单词数量:" + words.length);
		
		Map<String, Integer> map = new TreeMap<>();
		for (int i = 0; i < words.length; i++) {
			Integer count = map.get(words[i]);
			count = (count == null) ? 1 : (count + 1);
			map.put(words[i], count);
		}
		
		map.traversal(new Visitor<String, Integer>() {
			public boolean visit(String key, Integer value) {
				System.out.println(key + "_" + value);
				return false;
			}
		});
	}
}
public class Files {
	
	/**
	 * 读取文件内容
	 * @param file
	 * @return
	 */
	public static FileInfo read(String file) {
		if (file == null) return null;
		FileInfo info = new FileInfo();
		StringBuilder sb = new StringBuilder();
		try (FileReader reader = new FileReader(file);
				BufferedReader br = new BufferedReader(reader)) {
            String line;
            while ((line = br.readLine()) != null) {
            	sb.append(line).append("\n");
            	info.setLines(info.getLines() + 1);
            }
            int len = sb.length();
            if (len > 0) {
                sb.deleteCharAt(len - 1);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
		info.setFiles(info.getFiles() + 1);
		info.setContent(sb.toString());
		return info;
	}
	
	/**
	 * 读取文件夹下面的文件内容
	 * @param dir
	 * @param extensions
	 * @return
	 */
	public static FileInfo read(String dir, String[] extensions) {
		if (dir == null) return null;
		
		File dirFile = new File(dir);
		if (!dirFile.exists()) return null;

		FileInfo info = new FileInfo();
		dirFile.listFiles(new FileFilter() {
			public boolean accept(File subFile) {
				String subFilepath = subFile.getAbsolutePath();
				if (subFile.isDirectory()) {
					info.append(read(subFilepath, extensions));
				} else if (extensions != null && extensions.length > 0) {
					for (String extension : extensions) {
						if (subFilepath.endsWith("." + extension)) {
							info.append(read(subFilepath));
							break;
						}
					}
				} else {
					info.append(read(subFilepath));
				}
				return false;
			}
		});
		return info;
	}
}
public class FileInfo {
	private int lines;
	private int files;
	private String content = "";
	
	public String[] words() {
		return content.split("[^a-zA-Z]+");
	}
	
	public int getFiles() {
		return files;
	}

	public void setFiles(int files) {
		this.files = files;
	}

	public int getLines() {
		return lines;
	}
	
	public void setLines(int lines) {
		this.lines = lines;
	}
	
	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}

	public FileInfo append(FileInfo info) {
		if (info != null && info.lines > 0) {
			this.files += info.files;
			this.lines += info.lines;
			this.content = new StringBuilder(this.content)
					.append("\n")
					.append(info.content)
					.toString();
		}
		return this;
	}
}

八.哈希表

1. 理解

哈希表也叫做 散列表( hash 有“剁碎”的意思)

它是如何实现高效处理数据的?

  • put("Jack", 666);

  • put("Rose", 777);

  • put("Kate", 888);

添加、搜索、删除的流程都是类似的

  • 利用哈希函数生成 key 对应的 index【O(1)】

  • 根据 index 操作定位数组元素【O(1)】

哈希表是【空间换时间】的典型应用

哈希函数,也叫做 散列函数

哈希表内部的数组元素,很多地方也叫 Bucket(桶),整个数组叫 Buckets 或者 Bucket Array

clipboard (6)

注意:在实际应用中很多时候的需求:Map 中存储的元素不需要讲究顺序,Map 中的 Key 不需要具备可比较性。其实不考虑顺序、不考虑 Key 的可比较性,Map 有更好的实现方案,平均时间复杂度可以达到 O(1) ,那就是采取 哈希表来实现 Map

2. 哈希冲突(Hash Collision)

哈希冲突也叫做 哈希碰撞

  • 2 个不同的 key,经过哈希函数计算出相同的结果
  • key1 ≠ key2 ,hash(key1) = hash(key2)
clipboard (7)

解决哈希冲突的常见方法

  • 开放定址法(Open Addressing)即按照一定规则向其他地址探测,直到遇到空桶 。

  • 再哈希法(Re-Hashing)即设计多个哈希函数

  • 链地址法(Separate Chaining) 即比如通过链表将同一index的元素串起来

JDK1.8的哈希冲突解决方案

  • 默认使用 单向链表 将元素串起来(链地址法
  • 在添加元素时,可能会由 单向链表 转为 红黑树 来存储元素。比如当哈希表容量 ≥ 64 且 单向链表的节点数量大于 8 时
  • 红黑树 节点数量少到一定程度时,又会转为 单向链表
  • JDK1.8中的哈希表是使用 链表+红黑树解决哈希冲突
  • 思考一下这里为什么使用单链表?=> 每次都是从头节点开始遍历,单向链表比双向链表少一个指针,可以节省内存空间
clipboard (8)

3. 哈希函数

哈希表中哈希函数的实现步骤大概如下:

  • 先生成 key 的哈希值(必须是整数

  • 再让 key 的哈希值数组的大小 进行相关运算,生成一个 索引值

public int hash(Object key) { 
    return hash_code(key) % table.length;
}

为了提高效率,可以使用 & 位运算取代 % 运算【前提:将数组的长度设计为 2 的幂(2^n)

public int hash(Object key) { 
    return hash_code(key) & (table.length - 1);
}
//     1 = 2^0 
//    10 = 2^1 
//   100 = 2^2 
//  1000 = 2^3 
// 10000 = 2^4 
// 01111 = 2^4 - 1 = 1111

// ==> table.length - 1 表示 & 一个全为1的二进制数。结果必然小与这个全为1的二进制数

// 假设哈希值为1001010 => 
//   1001010
// & 0001111
// ----------
//   0001010   => 生成的值的范围是 0000 ~ 1111

良好的哈希函数 能让哈希值更加均匀分布 → 减少哈希冲突次数 → 提升哈希表的性能

此外,hashCode相等,生成的索引不一定相等。

11000
 &111
------
  000
  
10000
 &111
------
  000 

4. 如何生成key的哈希值

key 的常见种类可能有

  • 整数、浮点数、字符串、自定义对象

  • 不同种类的 key,哈希值的生成方式不一样,但目标是一致的

    • 尽量让每个 key 的哈希值是唯一的

    • 尽量让 key 的所有信息参与运算

在Java中,HashMap 的 key 必须实现 hashCodeequals 方法,也允许 key 为 null

整数

整数值当做哈希值

比如 10 的哈希值就是 10

public static int hashCode(int value) {
    return value;
}

浮点数

将存储的二进制格式转为整数值

public static int hashCode(float value) {
    return Float.floatToIntBits(value);
}

long

注意:Java的哈希值必须是 int 类型(32位)

public static int hashCode(long value) {
    //如果强制转换为int会直接砍掉前面32位,不推荐
    return (int)(value ^ (value >>> 32));
}
// 注意:>>> 和 ^ 的作用是?(>>>是无符号右移,^是异或运算)
//  ① 高32bit 和 低32bit 混合计算出 32bit 的哈希值
//  ② 充分利用所有信息计算出哈希值
// 另外:为什么不用 & 或者 |而是用 ^ ?
//  ① 如果value前32位全为1,使用 & 运算后32位就相当于没算了。
// ② 如果value前32位全为1,使用 | 运算后32位就全为1了。
clipboard (9)

double

public static int hashCode(double value) {
    long bits = doubleToLongBits(value);
    return (int)bits ^ (bits >>> 32);
}

字符串

先看一个问题:整数 5489 是如何计算出来的?

5 ∗ 10^3 + 4 ∗ 10^2 + 8 ∗ 10^1 + 9 ∗ 10^0 

字符串是由若干个字符组成的

  • 比如字符串 jack,由 j、a、c、k 四个字符组成(字符的本质就是一个整数,ASCII码)
  • 因此,jack 的哈希值可以表示为 j ∗ n^3 + a ∗ n^2 + c ∗ n^1 + k ∗ n^0,等价于 [ ( j ∗ n + a ) ∗ n + c ] ∗ n + k (等价后可以避免n的重复计算)
  • 在JDK中,乘数 n 为 31,为什么使用 31? => 31 是一个奇素数,JVM会将 31 * i 自动优化转化为 (i << 5) – i

注意:

① 31 * i = (2^5 - 1) * i = i * 2^5 - i = (i << 5) - i

② 31不仅仅是符合2^n - 1,它也是一个奇素数(既是奇数,也是质数。即质数)

=>素数和其他数相乘的结果比其他方式更容易产生唯一性,减少哈希冲突。

@Test
public void StrHashTest() {
	String str = "jack";
	int len = str.length();
	int hashCode = 0;
	for(int i = 0;i < len;i++) {
		char c = str.charAt(i);
		//hashCode = (hashCode << 5) - hashCode + c;
		hashCode = hashCode * 31 + c;
		// [ ( j ∗ n + a ) ∗ n + c ] ∗ n + k
	}
	System.out.println(hashCode);//3254239
	System.out.println(str.hashCode());//3254239
}

总结

@Test
public void hashTest() {
    int a = 110;
	float b = 10.6f;
	long c = 156l;
	double d = 10.9;
	String e = "rose";
		
	System.out.println(Integer.hashCode(a));
	System.out.println(Float.hashCode(b));
 	//System.out.println(Float.floatToIntBits(b)); //内部实现
	System.out.println(Long.hashCode(c));
	System.out.println(Double.hashCode(d));
	System.out.println(e.hashCode());
}

自定义对象的哈希值

自定义对象的hash值默认与该对象的内存地址有关。

注意:

① 哈希值太大,整型溢出怎么办? => 不用作任何处理,溢出了还是一个整 数。

② 不重写hashCode方法有什么后果? => 会以对象内存地址相关的值作为hash值。

重点:

① hashCode方法在在计算索引时调用

② equals方法在hash冲突时比较两个key是否相等时调用

④ 如果要求两个对象的哪些成员变量相等就代表这两个对象相等的话,hashCode方法和equals方法就只包含这些成员变量的计算就可以了。(hashCode方法必须要保证 equals 为 true 的 2 个key的哈希值一样,反过来hashCode相等的key,不一定equals为true)

public class HashTest {
	@Test
	public void PersonHashTest() {
		Person p1 = new Person(15,"rose",58.5f);
		Person p2 = new Person(15,"rose",58.5f);
		//System.out.println(p1.hashCode());//1834188994
		//System.out.println(p2.hashCode());//1174361318
		//=>自定义对象hash值默认与对象的地址值有关
		//√ 重写hashCode方法后:hash值相等意味着生成的索引相同
		System.out.println(p1.hashCode());//185317790
		System.out.println(p2.hashCode());//185317790
		
		Map<Object,Object> map = new HashMap<>();
		map.put(p1,"abc");
		map.put(p2,"bcd");//如果p1与p2"相等",就会覆盖,此时size为1才合理。
		//=>此时需要重写equals方法比较两个key是否"相等"
		//√ 注意:不能通过hash值的比较来判断两个key"相等",因为可能两个
		//       完全不同类型的key的hash值是相等的。
		System.out.println(map.size());//1
	}
}

class Person {
	private int age;
	private String name;
	private float height;

	public Person(int age, String name, float height) {
		super();
		this.age = age;
		this.name = name;
		this.height = height;
	}

	public Person() {
		super();
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public float getHeight() {
		return height;
	}

	public void setHeight(float height) {
		this.height = height;
	}

	@Override
	public String toString() {
		return "Person [age=" + age + ", name=" 
				+ name + ", height=" + height + "]";
	}
	
	/**
	 * 用来计算当前对象的hash值
	 */
	@Override
	public int hashCode() {
		//Integer.hashCode(age);
		//Float.hashCode(height);
		//name != null ? name.hashCode() : 0;
		int hashCode = Integer.hashCode(age);
		hashCode = hashCode * 31 + Float.hashCode(height);
		hashCode = hashCode * 31 + 
				(name != null ? name.hashCode() : 0);
		return hashCode;
	}

	/**
	 * 	用来比较两个对象是否相等
	 */
	@Override
	public boolean equals(Object obj) {
		if(this == obj) return true;
		//if(obj == null || obj instanceof Person) return false;
		if(obj == null || obj.getClass() != getClass()) return false;
		Person p = (Person)obj;
		return p.age == age 
			&& p.height == height
			&& p.name == null ? name == null : p.name.equals(name);
	}
}

5. HashMap实现

这里有如下设计

  • 直接使用红黑树解决hash冲突
  • 数组元素存储红黑树根节点,而不是存储红黑树对象。这样做的好处是就不用额外存储红黑树的size,comparator属性了(用不上)。
/**
 * @Description hashMap, hash冲突直接使用红黑树解决
 * @Author monap
 * @Date 2022/1/2 15:53
 */
@SuppressWarnings("unchecked")
public class HashMap<K, V> implements Map<K, V> {
    // 所有节点的数量
    private int size;
    private static final boolean RED = false;
    private static final boolean BLACK = true;
    private Node<K, V>[] table;
    private static final int DEFAULT_CAPACTIY = 1 << 4; // 默认容量
    private static final float DEFAULT_LOAD_FACTOR = 0.75f; // 装填因子

    protected static class Node<K, V> {
        int keyHash;
        K key;
        V value;
        boolean color = RED;
        Node<K, V> left; // 左子节点
        Node<K, V> right; // 右子节点
        Node<K, V> parent; // 父节点

        public Node(K key, V value, Node<K, V> parent) {
            this.key = key;
            int hash = key == null ? 0 : key.hashCode();
            this.keyHash = hash ^ (hash >>> 16); // 扰动计算一次,使hash值排布更均匀
            this.value = value;
            this.parent = parent;
        }

        public boolean isLeaf() {
            return left == null && right == null;
        }

        public boolean hasTwoChildren() {
            return left != null && right != null;
        }

        public boolean isLeftChild() {
            return parent != null && this == parent.left;
        }

        public boolean isRightChild() {
            return parent != null && this == parent.right;
        }

        public Node<K, V> getSibling() {
            if (isLeftChild()) {
                return parent.right;
            }
            if (isRightChild()) {
                return parent.left;
            }
            return null;
        }

        @Override
        public String toString() {
            return "Node_" + key + "_" + value;
        }
    }

    protected Node<K, V> createNode(K key, V value, Node<K, V> parent) {
        return new Node<>(key, value, parent);
    }

    /**
     * 获取key的hash值,并做一次扰动计算
     *
     * @param key
     * @return
     */
    private int hash(K key) {
        if (key == null) return 0;
        int hash = key.hashCode();
        return hash ^ (hash >>> 16);
    }

    /**
     * 根据key生成索引(在桶数组中的位置)
     *
     * @param key
     * @return
     */
    private int index(K key) {
        return hash(key) & (table.length - 1);
    }

    /**
     * 根据node获取索引(在桶数组中的位置)
     *
     * @param node
     * @return
     */
    private int index(Node<K, V> node) {
        return node.keyHash & (table.length - 1);
    }

    public HashMap() {
        table = new Node[DEFAULT_CAPACTIY];
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public void clear() {
        if (size == 0) return;
        size = 0;
        Arrays.fill(table, null);
    }

    @Override
    public V put(K key, V value) {
        // 检查是否需要扩容
        resize();
        int index = index(key);
        // 取出index位置的红黑树根节点
        Node<K, V> root = table[index];
        if (root == null) {
            root = createNode(key, value, null);
            table[index] = root;
            size++;
            afterPut(root);
            return null;
        }
        // hash冲突,添加新节点到红黑树
        // 1.找到待添加位置的父节点
        Node<K, V> parent = root;
        Node<K, V> node = root;
        int cmp = 0;
        K k1 = key;
        int h1 = hash(k1);
        Node<K, V> result = null;
        boolean searched = false;//是否已经搜索过这个key
        do {
            parent = node;
            K k2 = node.key;
            int h2 = node.keyHash;
            // 增加规则,先比较hash值,提高效率
            if (h1 > h2) {
                cmp = 1;
            } else if (h1 < h2) {
                cmp = -1;
            } else if (Objects.equals(k1, k2)) {
                cmp = 0;
            } else if (k1 != null && k2 != null
                    && k1.getClass() == k2.getClass()
                    && k1 instanceof Comparable
                    && (cmp = ((Comparable) k1).compareTo(k2)) != 0) { // 再次增加一条规则,提高效率
            } else if (searched) { // 已经扫描过了
                cmp = System.identityHashCode(k1) - System.identityHashCode(k2); // 最后增加一条规则,方便调试,但是不会增加效率
            } else { //未扫描,根据内存地址大小决定左右
                if ((node.left != null && (result = node(node.left, k1)) != null)
                        || (node.right != null && (result = node(node.right, k1)) != null)) {
                    //已经存在这个key
                    node = result;
                    cmp = 0;
                } else { // 不存在这个key
                    searched = true;
                    cmp = System.identityHashCode(k1) - System.identityHashCode(k2);
                }
            }
            if (cmp > 0) {
                node = node.right;
            } else if (cmp < 0) {
                node = node.left;
            } else {
                V oldValue = node.value;
                node.key = key;//一般覆盖(不同对象可能有相同的比较参数)
                node.value = value;
                return oldValue;
            }
        } while (node != null);
        // 2.判断插入父节点的左子节点还是右子节点
        Node<K, V> newNode = createNode(key, value, parent);
        if (cmp > 0) {
            parent.right = newNode;
        } else {
            parent.left = newNode;
        }
        size++;
        afterPut(newNode);//新添加节点之后的处理
        return null;
    }

    /**
     * 桶数组扩容
     */
    private void resize() {
        if (size / table.length <= DEFAULT_LOAD_FACTOR) return;
        // 扩容
        Node<K, V>[] oldTable = table;
        table = new Node[table.length << 1];
        // 分析:
        //   当扩容为原来的两倍时,节点的索引有2种情况
        //   1. 保持不变
        //   2. index = index + 旧容量
        // 层序遍历每个节点开始挪动
        Queue<Node<K, V>> queue = new LinkedList<>();
        for (Node<K, V> kvNode : oldTable) {
            if (kvNode == null) continue;
            queue.offer(kvNode);
            while (!queue.isEmpty()) {
                Node<K, V> node = queue.poll();
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
                moveNode(node);
            }
        }
    }

    private void moveNode(Node<K, V> newNode) {
        // 重置node
        newNode.parent = null;
        newNode.left = null;
        newNode.right = null;
        newNode.color = RED;
        int index = index(newNode);
        // 取出index位置的红黑树根节点
        Node<K, V> root = table[index];
        if (root == null) {
            root = newNode;
            table[index] = root;
            afterPut(root);
            return;
        }

        // hash冲突,添加新节点到红黑树
        // 1.找到待添加位置的父节点
        Node<K, V> parent = root;
        Node<K, V> node = root;
        int cmp = 0;
        K k1 = newNode.key;
        int h1 = newNode.keyHash;
        do {
            parent = node;
            K k2 = node.key;
            int h2 = node.keyHash;
            // 增加规则,先比较hash值,提高效率
            if (h1 > h2) {
                cmp = 1;
            } else if (h1 < h2) {
                cmp = -1;
            } else if (k1 != null && k2 != null
                    && k1.getClass() == k2.getClass()
                    && k1 instanceof Comparable
                    && (cmp = ((Comparable) k1).compareTo(k2)) != 0) { // 再次增加一条规则,提高效率
            } else {
                cmp = System.identityHashCode(k1) - System.identityHashCode(k2);
            }
            if (cmp > 0) {
                node = node.right;
            } else if (cmp < 0) {
                node = node.left;
            }
        } while (node != null);
        // 2.判断插入父节点的左子节点还是右子节点
        newNode.parent = parent;
        if (cmp > 0) {
            parent.right = newNode;
        } else {
            parent.left = newNode;
        }
        afterPut(newNode);//新添加节点之后的处理
    }

    private void afterPut(Node<K, V> node) {
        Node<K, V> parent = node.parent;
        //添加的是根节点 或 上溢到根节点
        if (parent == null) {
            black(node);
            return;
        }
        //类型一:parent是黑色(不用处理四种情况)
        if (isBlack(parent)) return;
        //类型二:parent是红色且uncle是红色(会上溢的四种情况)
        Node<K, V> uncle = parent.getSibling();
        Node<K, V> grand = red(parent.parent);//以下情况都需要将grand染成红色,可以统一处理
        if (isRed(uncle)) {
            black(parent);
            black(uncle);
            //把祖父节点当作是新添加的节点
            afterPut(grand);//上溢递归调用
            return;
        }
        //类型三:parent是红色且uncle不是红色(需要旋转的四种情况)
        if (parent.isLeftChild()) {//L
            if (node.isLeftChild()) { //LL
                black(parent);
            } else { //LR
                black(node);
                rotateLeft(parent);
            }
            rotateRight(grand);
        } else { //R
            if (node.isLeftChild()) { //RL
                black(node);
                rotateRight(parent);
            } else { //RR
                black(parent);
            }
            rotateLeft(grand);
        }
    }

    /**
     * 给节点上色
     */
    private Node<K, V> color(Node<K, V> node, boolean color) {
        if (node == null) return node;
        node.color = color;
        return node;
    }

    /**
     * 将节点染成红色
     */
    private Node<K, V> red(Node<K, V> node) {
        return color(node, RED);
    }

    /**
     * 将节点染成黑色
     */
    private Node<K, V> black(Node<K, V> node) {
        return color(node, BLACK);
    }

    /**
     * 获取当前节点的颜色
     */
    private boolean colorOf(Node<K, V> node) {
        return node == null ? BLACK : node.color;
    }

    /**
     * 判断当前颜色是否为黑色
     */
    private boolean isBlack(Node<K, V> node) {
        return colorOf(node) == BLACK;
    }

    /**
     * 判断当前颜色是否为红色
     */
    private boolean isRed(Node<K, V> node) {
        return colorOf(node) == RED;
    }

    /**
     * 左旋转,以RR为例
     */
    private void rotateLeft(Node<K, V> grand) {
        Node<K, V> parent = grand.right;
        Node<K, V> child = parent.left;//child就是T1子树
        grand.right = child;
        parent.left = grand;

        afterRotate(grand, parent, child);
    }

    /**
     * 右旋转,以LL为例
     */
    private void rotateRight(Node<K, V> grand) {
        Node<K, V> parent = grand.left;
        Node<K, V> child = parent.right;
        grand.left = child;
        parent.right = grand;

        afterRotate(grand, parent, child);
    }

    /**
     * 抽取左旋转和右旋转中的重复代码
     */
    private void afterRotate(Node<K, V> grand, Node<K, V> parent, Node<K, V> child) {
        //更新parent的parent(让parent成为子树的根节点)
        parent.parent = grand.parent;
        if (grand.isLeftChild()) {
            grand.parent.left = parent;
        } else if (grand.isRightChild()) {
            grand.parent.right = parent;
        } else { //grand是root节点
            table[index(grand)] = parent;
        }
        //更新child的parent
        if (child != null) {
            child.parent = grand;
        }
        //更新grand的parent
        grand.parent = parent;
    }

    @Override
    public V get(K key) {
        Node<K, V> node = node(key);
        return node != null ? node.value : null;
    }

    private Node<K, V> node(K key) {
        Node<K, V> root = table[index(key)];
        return root == null ? null : node(root, key);
    }

    private Node<K, V> node(Node<K, V> node, K k1) {
        int h1 = hash(k1);
        // 储存查找结果
        Node<K, V> result = null;
        int cmp = 0;
        while (node != null) {
            K k2 = node.key;
            int h2 = node.keyHash;
            // 增加规则,先比较hash值,提高效率
            if (h1 > h2) {
                node = node.right;
            } else if (h1 < h2) {
                node = node.left;
            } else if (Objects.equals(k1, k2)) {
                return node;
            } else if (k1 != null && k2 != null
                    && k1.getClass() == k2.getClass()
                    && k1 instanceof Comparable
                    && (cmp = ((Comparable) k1).compareTo(k2)) != 0) { // 再次增加一条规则,提高效率
                node = cmp > 0 ? node.right : node.left;
            } else if (node.right != null && (result = node(node.right, k1)) != null) { // 哈希值相等,不具备可比较性,也不equals
                return result;
            } else { //只能往左边扫
                node = node.left;
            }
        }
        return null;
    }

    @Override
    public V remove(K key) {
        return remove(node(key));
    }

    protected V remove(Node<K, V> node) {
        if (node == null) return null;
        Node<K, V> willNode = node;
        size--;
        V oldValue = node.value;
        //考虑度为2的节点,转化为度为1
        if (node.hasTwoChildren()) {
            Node<K, V> s = successor(node);//后继节点
            //用后继节点的值覆盖度为2的节点的值
            node.key = s.key;
            node.value = s.value;
            node.keyHash = s.keyHash;
            //删除后继节点
            node = s;
        }
        //删除node节点(能到这则说明node的度必为0或1)
        Node<K, V> replacement = node.left != null ? node.left : node.right;
        int index = index(node);
        if (replacement != null) { //node是度为1的节点
            //更改parent
            replacement.parent = node.parent;
            //更改parent的left,right指向
            if (node.parent == null) { //node是度为1的节点也是根节点
                table[index] = replacement;
            } else if (node == node.parent.left) {
                node.parent.left = replacement;
            } else { //在右边
                node.parent.right = replacement;
            }
            //此时开始恢复平衡(AVL树 或 RB树需要实现此方法)
            afterRemove(node, replacement);
        } else if (node.parent == null) { //node是叶子节点也是根节点
            table[index] = null;
            afterRemove(node, null);
        } else { //node是叶子节点但不是根节点
            if (node == node.parent.left) {
                node.parent.left = null;
            } else {
                node.parent.right = null;
            }
            //此时开始恢复平衡(AVL树 或RB树 需要实现此方法)
            afterRemove(node, null);
        }
        // 交给子类去处理
        subclassAfterRemove(willNode, node);
        return oldValue;
    }

    protected void subclassAfterRemove(Node<K, V> willNode, Node<K, V> removedNode) {}

    /**
     * 实现删除节点后的处理操作
     */
    private void afterRemove(Node<K, V> node, Node<K, V> replacement) {
        //情况一:如果删除的节点是红色,不用处理
        if (isRed(node)) return;
        //情况二:用于取代node子节点的是红色节点
        if (isRed(replacement)) {
            black(replacement);
            return;
        }
        //情况三:删除的是黑色叶子节点(下溢)
        Node<K, V> parent = node.parent;
        //删除的是根节点
        if (parent == null) return;
        //判断被删除的node的节点是左还是右
        boolean left = parent.left == null || node.isLeftChild();
        Node<K, V> sibling = left ? parent.right : parent.left;
        if (left) { //被删除的节点在左边,兄弟节点在右边(镜像对称处理)
            if (isRed(sibling)) { //兄弟节点是红色,就要转成黑色
                black(sibling);
                red(parent);
                rotateLeft(parent);
                //更换兄弟
                sibling = parent.right;
            }
            //兄弟节点必然是黑色
            if (isBlack(sibling.left) && isBlack(sibling.right)) {
                //兄弟节点没有一个红色子节点,父节点要向下向子节点合并
                boolean parentBlack = isBlack(parent);
                black(parent);
                red(sibling);
                if (parentBlack) {
                    afterRemove(parent, null);
                }
            } else { //兄弟节点至少有 1 个红色节点,就要向兄弟节点借元素
                if (isBlack(sibling.right)) {
                    //兄弟节点的右边不是红色,则兄弟要先旋转
                    rotateRight(sibling);
                    sibling = parent.right;
                }
                color(sibling, colorOf(parent));
                black(sibling.right);
                black(parent);
                rotateLeft(parent);
            }
        } else { //被删除的节点在右边,兄弟节点在左边(图示的是这种)
            if (isRed(sibling)) { //兄弟节点是红色,就要转成黑色
                black(sibling);
                red(parent);
                rotateRight(parent);
                //更换兄弟
                sibling = parent.left;
            }
            //兄弟节点必然是黑色
            if (isBlack(sibling.left) && isBlack(sibling.right)) {
                //兄弟节点没有一个红色子节点,父节点要向下向子节点合并
                boolean parentBlack = isBlack(parent);
                black(parent);
                red(sibling);
                if (parentBlack) {
                    afterRemove(parent, null);
                }
            } else { //兄弟节点至少有 1 个红色节点,就要向兄弟节点借元素
                if (isBlack(sibling.left)) {
                    //兄弟节点的左边不是红色,则兄弟要先旋转
                    rotateLeft(sibling);
                    sibling = parent.left;
                }
                color(sibling, colorOf(parent));
                black(sibling.left);
                black(parent);
                rotateRight(parent);
            }
        }
    }

    /**
     * 利用中序遍历求某个节点的后继节点
     */
    private Node<K, V> successor(Node<K, V> node) {
        if (node == null) return null;
        //前驱节点在右子树中:node.right.left.left...
        Node<K, V> p = node.right;
        if (p != null) {
            while (p.left != null) {
                p = p.left;
            }
            return p;
        }
        //从祖父节点中寻找前驱节点
        while (node.parent != null && node == node.parent.right) {
            node = node.parent;
        }
        //情况一:node.parent == null ↓
        //情况二:node == node.parent.left ↓
        return node.parent;
    }


    @Override
    public boolean containsKey(K key) {
        return node(key) != null;
    }

    /**
     * 遍历每一个节点的value,红黑树使用层序遍历
     *
     * @param value 每一个节点的value
     * @return bool
     */
    @Override
    public boolean containsValue(V value) {
        if (size == 0) return false;
        Queue<Node<K, V>> queue = new LinkedList<>();
        for (Node<K, V> kvNode : table) {
            if (kvNode == null) continue;
            queue.offer(kvNode);
            while (!queue.isEmpty()) {
                Node<K, V> node = queue.poll();
                if (Objects.equals(value, node.value)) return true;
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }
        }
        return false;
    }

    @Override
    public void traversal(Visitor<K, V> visitor) {
        if (size == 0 || visitor == null) return;
        Queue<Node<K, V>> queue = new LinkedList<>();
        for (Node<K, V> kvNode : table) {
            if (kvNode == null) continue;
            queue.offer(kvNode);
            while (!queue.isEmpty()) {
                Node<K, V> node = queue.poll();
                if (visitor.visit(node.key, node.value)) return;
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }
        }
    }

    public void print() {
        if (size == 0) return;
        for (final Node<K, V> root : table) {
            BinaryTrees.println(new BinaryTreeInfo() {
                @Override
                public Object root() {
                    return root;
                }

                @Override
                public Object left(Object node) {
                    return ((Node<K, V>) node).left;
                }

                @Override
                public Object right(Object node) {
                    return ((Node<K, V>) node).right;
                }

                @Override
                public Object string(Object node) {
                    return node;
                }
            });
        }
    }
}

6. 哈希值的进一步处理:扰动计算

在上面的hashmap实现中,生成hash值时为什么还要再次高低16位做与运算?

==> 扰动计算,能使hash排布更加均匀!

private int hash(K key) {
    if (key == null) return 0;
    int h = key.hashCode();
    return (h ^ (h >>> 16)) & (table.length - 1);
}

7. 装填因子

在上面的hashmap实现中,在扩容时用到了 装填因子 !

装填因子(Load Factor):节点总数量 / 哈希表桶数组长度,也叫做负载因子

在JDK1.8的HashMap中,如果装填因子超过0.75,就扩容为原来的2倍

8. 关于使用%来计算索引

如果使用%来计算索引

  • 建议把哈希表的长度设计为素数(质数),可以大大减小哈希冲突
image-20220101165226216

下边表格列出了不同数据规模对应的最佳素数,特点如下

  • 每个素数略小于前一个素数的2倍
  • 每个素数尽可能接近2的幂(2^n)
image-20220101165328974

9. TreeMap VS HashMap

9.1 性能对比

image-20220103011201320

9.2 选择时机

何时选择 TreeMap? => 元素具备可比较性且要求升序遍历(按照元素从小到大)

何时选择 HashMap?=> 无序遍历

10.LinkedHashMap

10.1 理解

在HashMap的基础上维护元素的添加顺序,使得遍历的结果是遵从添加顺序的

假设添加顺序是:37、21、31、41、97、95、52、42、83

image-20220101164517665

10.2 LinkedHashMap实现

/**
 * @Description LinkedHashMap, 采取双向链表,提高效率
 * @Author monap
 * @Date 2022/1/2 15:53
 */
@SuppressWarnings("unchecked")
public class LinkedHashMap<K, V> extends HashMap<K, V> {
    private LinkedHashNode<K, V> first;
    private LinkedHashNode<K, V> last;

    private static class LinkedHashNode<K, V> extends Node<K, V> {
        LinkedHashNode<K, V> prev;
        LinkedHashNode<K, V> next;

        public LinkedHashNode(K key, V value, Node<K, V> parent) {
            super(key, value, parent);
        }
    }

    @Override
    protected Node<K, V> createNode(K key, V value, Node<K, V> parent) {
        LinkedHashNode<K, V> node = new LinkedHashNode<>(key, value, parent);
        if (first == null) {
            first = last = node;
        } else {
            last.next = node;
            node.prev = last;
            last = node;
        }
        return node;
    }

    @Override
    public void clear() {
        super.clear();
        first = null;
        last = null;
    }

    @Override
    protected void subclassAfterRemove(Node<K, V> willNode,Node<K, V> removedNode) {
        LinkedHashNode<K, V> node1 = (LinkedHashNode<K, V>) willNode;
        LinkedHashNode<K, V> node2 = (LinkedHashNode<K, V>) removedNode;
        if(node1 != node2) {
            // 交换linkedHashWillNode和linkedHashRemovedNode在链表中的位置
            // 交换prev
            LinkedHashNode<K,V> tmp = node1.prev;
            node1.prev = node2.prev;
            node2.prev = tmp;
            if(node1.prev == null) {
                first = node1;
            } else {
                node1.prev.next = node1;
            }
            if(node2.prev == null) {
                first = node2;
            } else {
                node2.prev.next = node2;
            }
            //交换last
            tmp = node1.next;
            node1.next = node2.next;
            node2.next = tmp;
            if(node1.next == null) {
                last = node1;
            } else {
                node1.next.prev = node1;
            }
            if(node2.next == null) {
                last = node2;
            } else {
                node2.next.prev = node2;
            }
        }

        LinkedHashNode<K, V> prev = node2.prev;
        LinkedHashNode<K, V> next = node2.next;
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
        }
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
        }
    }

    @Override
    public boolean containsValue(V value) {
        LinkedHashNode<K, V> node = first;
        while (node != null) {
            if (Objects.equals(value,node.value)) return true;
            node = node.next;
        }
        return false;
    }

    @Override
    public void traversal(Visitor<K, V> visitor) {
        if (visitor == null) return;
        LinkedHashNode<K, V> node = first;
        while (node != null) {
            if (visitor.visit(node.key, node.value)) return;
            node = node.next;
        }
    }
}

注意:链表是跨红黑树的!

10.3 删除的注意点

删除度为2的节点node时(比如删除31),需要注意更换 node 与 前驱\后继节点 的连接位置

image-20220101164658085

10.4 更换节点的连接位置

image-20220101164731371

交换prev

LinkedNode<K,V> tmp = node1.prev;
node1.prev = node2.prev;
node2.prev = tmp;
if (node1.prev != null) {
    node1.prev.next = node1;
} else {
    first = node1;
}

if (node2.prev != null) {
	node2.prev.next = node2;
} else {
    first = node2;
}

交换next

tmp = node.next;
node1.next = node2.next;
node2.next = tmp;
if (node1.next != null) {
    node1.next.prev = node1;
} else {
    last = node1;
}

if (node2.next != null) {
	node2.next.prev = node2;
} else {
    last = node2;
}

11. HashSet

/**
 * @Description TODO
 * @Author monap
 * @Date 2022/1/3 2:25
 */
public class HashSet<E> implements Set<E> {
    private HashMap<E,Object> map = new HashMap<>();

    @Override
    public int size() {
        return map.size();
    }

    @Override
    public boolean isEmpty() {
        return map.isEmpty();
    }

    @Override
    public void clear() {
        map.clear();
    }

    @Override
    public boolean contains(E element) {
        return map.containsKey(element);
    }

    @Override
    public void add(E element) {
        map.put(element,null);
    }

    @Override
    public void remove(E element) {
        map.remove(element);
    }

    @Override
    public void traversal(Visitor<E> visitor) {
        map.traversal(new Map.Visitor<E,Object>() {
            @Override
            public boolean visit(E key, Object value) {
                return visitor.visit(key);
            }
        });
    }
}

12. LinkedHashSet

/**
 * @Description TODO
 * @Author monap
 * @Date 2022/1/3 2:25
 */
public class LinkedHashSet<E> implements Set<E> {
    private LinkedHashMap<E,Object> map = new LinkedHashMap<>();

    @Override
    public int size() {
        return map.size();
    }

    @Override
    public boolean isEmpty() {
        return map.isEmpty();
    }

    @Override
    public void clear() {
        map.clear();
    }

    @Override
    public boolean contains(E element) {
        return map.containsKey(element);
    }

    @Override
    public void add(E element) {
        map.put(element,null);
    }

    @Override
    public void remove(E element) {
        map.remove(element);
    }

    @Override
    public void traversal(Visitor<E> visitor) {
        map.traversal(new Map.Visitor<E,Object>() {
            @Override
            public boolean visit(E key, Object value) {
                return visitor.visit(key);
            }
        });
    }
}

九. 二叉堆

1. 引入

设计一种数据结构,用来存放整数,要求提供 3 个接口

  • 添加元素
  • 获取最大值
  • 删除最大值
image-20220103025909450

有没有更优的数据结构?=> 堆

  • 获取最大值:O(1)
  • 删除最大值:O(logn)
  • 添加元素:O(logn)

解决 Top K 问题

什么是 Top K 问题? => 从海量数据中找出前 K 个数据,比如从 100 万个整数中找出最大的 100 个整数

Top K 问题的解法之一:可以用数据结构“堆”来解决

2. 堆理解

堆(Heap)也是一种树状的数据结构(不要跟内存模型中的“堆空间”混淆),常见的堆实现有

  • 二叉堆(Binary Heap,完全二叉堆)
  • 多叉堆(D-heap、D-ary Heap)
  • 索引堆(Index Heap)
  • 二项堆(Binomial Heap)
  • 斐波那契堆(Fibonacci Heap)
  • 左倾堆(Leftist Heap,左式堆)
  • 斜堆(Skew Heap)

堆的一个重要性质:任意节点的值总是 ≥( ≤ )子节点的值

  • 如果任意节点的值总是 ≥ 子节点的值,称为:最大堆、大根堆、大顶堆

  • 如果任意节点的值总是 ≤ 子节点的值,称为:最小堆、小根堆、小顶堆

由此可见,堆中的元素必须具备可比较性(跟二叉搜索树一样)

3. 堆基本接口设计

image-20220103030637581

4. 二叉堆理解

二叉堆 的逻辑结构就是一棵完全二叉树,所以也叫 完全二叉堆

鉴于完全二叉树的一些特性,二叉堆的底层(物理结构)一般用数组实现即可

索引 i 的规律( n 是元素数量)

  • 如果 i = 0 ,它是 节点
  • 如果 i > 0 ,它的 节点的索引为 floor( (i – 1) / 2 )
  • 如果 2i + 1 ≤ n – 1,它的 子节点的索引为 2i + 1
  • 如果 2i + 1 > n – 1 ,它 无左子节点
  • 如果 2i + 2 ≤ n – 1 ,它的 子节点的索引为 2i + 2
  • 如果 2i + 2 > n – 1 ,它 无右子节点
image-20220103031011454

5. 最大堆-添加

image-20220105224635687

循环执行以下操作(图中的 80 简称为 node)

  • 如果 node > 父节点 ==> 与父节点交换位置
  • 如果 node ≤ 父节点,或者 node 没有父节点 ==> 退出循环

这个过程,叫做上滤(Sift Up),时间复杂度为 O(logn)

交换位置的优化

一般交换位置需要3行代码,可以进一步优化 ==> 将新添加节点备份,确定最终位置才摆放上去

仅从交换位置的代码角度看,可以由大概的 3 * O(logn) 优化到 1 * O(logn) + 1

image-20220105225124705

6. 最大堆-删除

image-20220105232454567
  • 用最后一个节点覆盖根节点

  • 删除最后一个节点

  • 循环执行以下操作(图中的 43 简称为 node)

    • 如果 node < 最大的子节点 ==> 与最大的子节点交换位置
    • 如果 node ≥ 最大的子节点, 或者 node 没有子节点 ==> 退出循环

这个过程,叫做下滤(Sift Down),时间复杂度:O(logn)

同样的,交换位置的操作可以像添加那样进行优化

7. 最大堆–批量建堆 (Heapify)

批量建堆,有 2 种做法

  • 自上而下的上滤
  • 自下而上的下滤

自上而下的上滤

image-20220105235809786

自下而上的下滤

image-20220105235841884

效率对比

image-20220105235916423

所有节点的深度之和

  • 仅仅是叶子节点,就有近 n/2 个,而且每一个叶子节点的深度都是 O(logn) 级别的
  • 因此,在叶子节点这一块,就达到了 O(nlogn) 级别
  • O(nlogn) 的时间复杂度足以利用排序算法对所有节点进行全排序

所有节点的高度之和

  • 假设是满树,节点总个数为 n,树高为 h,那么 n = 2^h − 1
  • 所有节点的树高之和
H(n) = 2^0 ∗ (h − 0) + 2^1 ∗ (h − 1) + 2^2 ∗ (h − 2) + ⋯ + 2^(h −1) ∗ [h − (h −1)]
     = h ∗ (2^0 + 2^1 + 2^2 + ⋯ + 2^(h −1) − [1 ∗ 2^1 + 2 ∗ 2^2 + 3 ∗ 2^3 + ⋯ + (h − 1) ∗ 2^(h−1)
     = h ∗ (2^h − 1) − [(h − 2) ∗ 2^h + 2]
     = h ∗ 2^h − h − h ∗ 2^h + 2 ^(h+1) − 2
     = 2^(h+1) − h − 2
     = 2 ∗ (2^h − 1) − h
     = 2n − h
     = 2n − log2(n + 1)
     = O(n)

公式推导

S(h) = 1 ∗ 2^1 + 2 ∗ 2^2 + 3 ∗ 2^3 + ⋯ + (h − 2) ∗ 2^(h−2) + (h − 1) ∗ 2^(h−1)

2S(h) = 1 ∗ 2^2 + 2 ∗ 2^3 + 3 ∗ 2^4 + ⋯ + (h − 2) ∗ 2^(h−1) + (h − 1)  ∗ 2^h

S(h) – 2S(h) = [2^1 + 2^2 + 2^3 + ⋯ + 2^(h−1)] − (h − 1)  ∗ 2^h 
             = (2^h − 2) −  (h − 1) ∗ 2^h

S(h) = (h − 1) ∗ 2^h − (2^h − 2) 
     = (h − 2) ∗ 2^h + 2

疑惑

以下方法可以批量建堆么

  • 自上而下的下滤
  • 自下而上的上滤

上述方法不可行,为什么?

认真思考【自上而下的上滤】、【自下而上的下滤】的本质。自上而下的上滤的本质是添加,自下而上的下滤的本质是删除

8. 构建小顶堆

只需要改变一下比较策略即可,比如值比较小的节点更大

@Test
public void testMinHeap() {
    Integer[] data = {88, 44, 53, 41, 16, 6, 70, 18, 85, 98, 81, 23};
    BinaryHeap<Integer> heap = new BinaryHeap<>(data, new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    });
    BinaryTrees.println(heap);
}

9. 大顶堆实现

抽象父类

/**
 * @Description 二叉堆
 * @Author monap
 * @Date 2022/1/5 23:17
 */
@SuppressWarnings("unchecked")
public abstract  class AbstractHeap<E> implements Heap<E> {
    protected int size;
    protected Comparator<E> comparator;

    public AbstractHeap(Comparator<E> comparator) {
        this.comparator = comparator;
    }

    public AbstractHeap() {
        this(null);
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    protected int compare(E e1, E e2) {
        return comparator != null ? comparator.compare(e1, e2) : ((Comparable<E>) e1).compareTo(e2);
    }
}

具体类

/**
 * @Description 二叉堆
 * @Author monap
 * @Date 2022/1/5 22:30
 */
@SuppressWarnings("unchecked")
public class BinaryHeap<E> extends AbstractHeap<E> implements BinaryTreeInfo {
    private E[] elements;
    private static final int DEFAULT_CAPACITY = 10;

    public BinaryHeap(E[] elements, Comparator<E> comparator) {
        super(comparator);
        if (elements == null || elements.length == 0) {
            this.elements = (E[]) new Object[DEFAULT_CAPACITY];
        } else {
            size = elements.length;
            int capacity = Math.max(elements.length, DEFAULT_CAPACITY);
            this.elements = (E[]) new Object[capacity];
            System.arraycopy(elements, 0, this.elements, 0, capacity);
            heapify();
        }
    }

    public BinaryHeap(E[] elements) {
        this(elements, null);
    }

    public BinaryHeap(Comparator<E> comparator) {
        this(null, comparator);
    }

    public BinaryHeap() {
        this(null, null);
    }

    /**
     * 批量建堆
     */
    private void heapify() {
        // 1.自上而下的上滤
//        for (int i = 0; i < size; i++) {
//            siftUp(i);
//        }

        // 2. 自下而上的下滤
        for (int i = (size >> 1) - 1; i >= 0; i--) {
            siftDown(i);
        }
    }

    @Override
    public void clear() {
        for (int i = 0; i < size; i++) {
            elements[i] = null;
        }
    }

    @Override
    public void add(E element) {
        elementNotNullCheck(element);
        ensureCapacity(size + 1);
        elements[size++] = element;
        siftUp(size - 1);
    }

    /**
     * 让index位置的元素上滤
     *
     * @param index index
     */
    private void siftUp(int index) {
        E e = elements[index];
        while (index > 0) {
            int pIndex = (index - 1) >> 1;
            E p = elements[pIndex];
            if (compare(e, p) <= 0) break;
            elements[index] = p;
            index = pIndex;
        }
        elements[index] = e;
    }

    @Override
    public E get() {
        emptyCheck();
        return elements[0];
    }

    /**
     * 删除堆顶元素
     **/
    @Override
    public E remove() {
        emptyCheck();
        int lastIndex = --size;
        E root = elements[0];
        elements[0] = elements[lastIndex];
        elements[lastIndex] = null;
        siftDown(0);
        return root;
    }

    private void siftDown(int index) {
        E element = elements[index];
        // 第一个叶子节点的索引即为非叶子节点的数量
        int half = size >> 1;
        // 必须保证index位置必须为非叶子节点
        while (index < half) {
            //index的节点有两种情况
            // 1.只有左子节点
            // 2.同时拥有左右节点
            // 默认为左子节点的索引跟它比较
            int childIndex = (index << 1) + 1;
            E child = elements[childIndex];
            // 右子节点
            int rightIndex = childIndex + 1;
            // 选出左右子节点中最大的那个
            if (rightIndex < size && compare(elements[rightIndex], child) > 0) {
                child = elements[childIndex = rightIndex];
            }
            if (compare(element, child) >= 0) break;
            //将子节点存放到index位置
            elements[index] = child;
            //重新设置index
            index = childIndex;
        }
        elements[index] = element;
    }

    /**
     * 删除堆顶元素的同时插入一个新元素
     *
     * @param element element
     * @return E
     */
    @Override
    public E replace(E element) {
        elementNotNullCheck(element);
        E root = null;
        if (size == 0) {
            elements[0] = element;
            size++;
        } else {
            root = elements[0];
            elements[0] = element;
            siftDown(0);
        }
        return root;
    }


    private void emptyCheck() {
        if (size == 0) {
            throw new IndexOutOfBoundsException("Heap is empty");
        }
    }

    private void ensureCapacity(int capacity) {
        int oldCapacity = elements.length;
        if (capacity < oldCapacity) return;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍
        E[] newElements = (E[]) new Object[newCapacity];
        System.arraycopy(elements, 0, newElements, 0, elements.length);
        elements = newElements;
        System.out.println("扩容:" + oldCapacity + "=>" + newCapacity);
    }

    private void elementNotNullCheck(E element) {
        if (element == null) {
            throw new IllegalArgumentException("element mush not be empty");
        }
    }

    @Override
    public Object root() {
        return 0;
    }

    @Override
    public Object left(Object node) {
        int index = ((int) node << 1) + 1;
        return index >= size ? null : index;
    }

    @Override
    public Object right(Object node) {
        int index = ((int) node << 1) + 2;
        return index >= size ? null : index;
    }

    @Override
    public Object string(Object node) {
        return elements[(int) node];
    }
}

10. Top K 问题

从 n 个整数中,找出最大的前 k 个数( k 远远小于 n )

如果使用排序算法进行全排序,需要 O(nlogn) 的时间复杂度

如果使用二叉堆来解决,可以使用 O(nlogk) 的时间复杂度来解决

  • 新建一个小顶堆
  • 扫描 n 个整数
    • 先将遍历到的前 k 个数放入堆中
    • 从第 k + 1 个数开始,如果大于堆顶元素,就使用 replace 操作(删除堆顶元素,将第 k + 1 个数添加到堆中)
  • 扫描完毕后,堆中剩下的就是最大的前 k 个数

如果是找出最小的前 k 个数呢?

  • 用大顶堆
  • 如果小于堆顶元素,就使用 replace 操作
/**
 * 找出下面数组中最大的5个数
 */
@Test
public void testTopK() {
    int k = 5;
    Integer[] data = {51, 30, 39, 92, 74, 25, 16, 93,
                      91, 19, 54, 47, 73, 62, 76, 63, 35, 18,
                      90, 6, 65, 49, 3, 26, 61, 48};
    BinaryHeap<Integer> heap = new BinaryHeap<>((o1, o2) -> o2 - o1);
    for (Integer datum : data) {
        if (heap.size() < k) {
            heap.add(datum);
        } else if (datum > heap.get()) {
            heap.replace(datum);
        }
    }
    BinaryTrees.println(heap);
}
//     ┌─76─┐
//     │    │
//  ┌─90─┐  93
//  │    │
// 92    91

十. 优先级队列

1. 接口设计

优先级队列也是个队列,因此也是提供以下接口

image-20220106012111683

普通的队列是 FIFO 原则,也就是先进先出

优先级队列则是按照优先级高低进行出队,比如将优先级最高的元素作为队头优先出队

2. 应用场景

医院的夜间门诊

  • 队列元素是病人
  • 优先级是病情的严重情况、挂号时间

操作系统的多任务调度

  • 队列元素是任务

  • 优先级是任务类型

3. 实现

根据优先队列的特点,很容易想到:可以直接利用二叉堆作为优先队列的底层实现

可以通过 Comparator 或 Comparable 去自定义优先级高低

/**
 * @Description 优先级队列
 * @Author monap
 * @Date 2022/1/6 1:26
 */
public class PriorityQueue<E> {
    private BinaryHeap<E> heap;

    public PriorityQueue(Comparator<E> comparator) {
        heap = new BinaryHeap<>(comparator);
    }

    public PriorityQueue() {
        this(null);
    }

    //元素的数量
    public int size() {
        return heap.size();
    }

    // 判断队列是否为空
    public boolean isEmpty() {
        return heap.isEmpty();
    }

    // 入队
    public void enQueue(E element) {
        heap.add(element);
    }

    // 出队
    public E deQueue() {
        return heap.remove();
    }

    // 清空队列元素
    public void clear() {
        heap.clear();
    }

    // 获取优先级最高的元素
    public E front() {
        return heap.get();
    }
}

十一. 哈夫曼树

1. 哈夫曼编码

哈夫曼编码,又称为霍夫曼编码(Huffman Coding),它是现代压缩算法的基础

假设要把字符串【ABBBCCCCCCCCDDDDDDEE】转成二进制编码进行传输

  • 可以转成ASCII编码(6569,10000011000101),但是有点冗长,如果希望编码更短呢?

  • 可以先约定5个字母对应的二进制。对应的二进制编码:000001001001010010010010010010010010011011011011011011100100 ,一共20个字母,转成了60个二进制位

image-20220106014306287
  • 如果使用哈夫曼编码,可以压缩至41个二进制位,约为原来长度的68.3%

2. 哈夫曼树

先计算出每个字母的出现频率(权值,这里直接用出现次数),【ABBBCCCCCCCCDDDDDDEE】

image-20220106014435079

利用这些权值,构建一棵哈夫曼树(又称为霍夫曼树、最优二叉树)

如何构建一棵哈夫曼树?(假设有 n 个权值)

  • 以权值作为根节点构建 n 棵二叉树,组成森林
  • 在森林中选出 2 个根节点最小的树合并,作为一棵新树的左右子树,且新树的根节点为其左右子树根节点之和
  • 从森林中删除刚才选取的 2 棵树,并将新树加入森
  • 重复 2、3 步骤,直到森林只剩一棵树为止,该树即为哈夫曼树

3. 构建哈夫曼树

image-20220106014544666

4. 构建哈夫曼编码

image-20220106014637345

left为0,right为1,可以得出5个字母对应的哈夫曼编码

image-20220106014709221

【ABBBCCCCCCCCDDDDDDEE】的哈夫曼编码是 1110110110110000000001010101010101111

总结

  • n 个权值构建出来的哈夫曼树拥有 n 个叶子节点
  • 每个哈夫曼编码都不是另一个哈夫曼编码的前缀
  • 哈夫曼树是带权路径长度最短的树,权值较大的节点离根节点较近
  • 带权路径长度:树中所有的叶子节点的权值乘上其到根节点的路径长度。与最终的哈夫曼编码总长度成正比关系。

十二 Trie 字典树

1. 引入

需求

如何判断一堆不重复的字符串是否以某个前缀开头?我们可以用Set或Map存储字符串,遍历所有字符串进行判断。时间复杂度为O(n)

有没有更优的数据结构实现前缀搜索?有!那就是Trie

Trie理解

Trie 也叫做字典树、前缀树(Prefix Tree)、单词查找树

Trie 搜索字符串的效率主要跟字符串的长度有关

假设使用 Trie 存储 cat、dog、doggy、does、cast、add 六个单词

image-20220106014851941

2. 接口设计

有两种接口形式,可以分别用Set和Map实现。Map可以做到在存储字符串的同时储存其对应的value(如人的姓名和其对应的电话号码)

image-20220106212223906

3. 实现

/**
 * @Description 字典树
 * @Author monap
 * @Date 2022/1/6 21:19
 */
public class Trie<V> {
    private int size;
    private Node<V> root;

    private static class Node<V> {
        Node<V> parent;
        HashMap<Character, Node<V>> children;
        Character character;
        V value;
        boolean word; // 是否为单词

        public Node(Node<V> parent) {
            this.parent = parent;
        }
    }

    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public void clear() {
        size = 0;
        root = null;
    }

    public V get(String key) {
        Node<V> node = node(key);
        return node != null && node.word ? node.value : null;
    }

    public boolean contains(String key) {
        Node<V> node = node(key);
        return node != null && node.word;
    }

    public V add(String key, V value) {
        keyCheck(key);
        if (root == null) {
            root = new Node<>(null);
        }
        Node<V> node = root;
        int len = key.length();
        for (int i = 0; i < len; i++) {
            char c = key.charAt(i);
            boolean emptyChildren = node.children == null;
            Node<V> childNode = emptyChildren ? null : node.children.get(c);
            if (childNode == null) {
                childNode = new Node<>(node);
                childNode.character = c;
                node.children = emptyChildren ? new HashMap<>() : node.children;
                node.children.put(c, childNode);
            }
            node = childNode;
        }
        if (node.word) {
            V oldValue = node.value;
            node.value = value;
            return oldValue;
        }
        node.word = true;
        node.value = value;
        size++;
        return null;
    }

    public V remove(String key) {
        // 找到最后一个节点
        Node<V> node = node(key);
        // 如果不是单词结尾,不做任何处理
        if (node == null || !node.word) return null;
        size--;
        V oldValue = node.value;
        // 如果还有子节点
        if (node.children != null && !node.children.isEmpty()) {
            node.word = false;
            node.value = null;
            return oldValue;
        }
        // 如果没有子节点
        Node<V> parent;
        while ((parent = node.parent) != null) {
            parent.children.remove(node.character);
            if (parent.word || !parent.children.isEmpty()) break;
            node = parent;
        }
        return oldValue;
    }

    public boolean startWith(String prefix) {
        return node(prefix) != null;
    }

    private Node<V> node(String key) {
        keyCheck(key);
        Node<V> node = root;
        int len = key.length();
        for (int i = 0; i < len; i++) {
            if (node == null || node.children == null || node.children.isEmpty()) return null;
            char c = key.charAt(i);
            node = node.children.get(c);
        }
        return node;
    }

    private void keyCheck(String key) {
        if (key == null || key.length() == 0) {
            throw new IllegalArgumentException("key must not empty");
        }
    }
}

4. 总结

Trie 的优点:搜索前缀的效率主要跟前缀的长度有关

Trie 的缺点:需要耗费大量的内存,因此还有待改进

更多Trie 相关的数据结构和算法

  • Double-array Trie
  • Suffix Tree
  • Patricia Tree
  • Crit-bit Tree
  • AC自动机
posted @ 2022-01-07 00:59  MPolaris  阅读(120)  评论(0编辑  收藏  举报