Java教程

数据结构(java版)

本文主要是介绍数据结构(java版),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

复杂度

什么是算法

算法是用于解决特定问题一系列执行步骤

如果单从执行效率上进行评估,可能会想到这么一种方案比较不同算法对同一组输入的执行处理时间,这种叫事后统计法

评估算法优劣

时间复杂度:程序指令执行次数

空间复杂度:估算所需占用的存储空间

大O表示法

表示数据规模n对应的复杂度

9  >> O(1)
2n+3 >> O(n^2)
n^2^

9 >> O(1)
2n+3 >> O(n2)
n2 +2n +6 >> O(n2)

n3 +2n +6 >> O(n3)

对数阶的细节

所以 log2n 、log9n 统称为 logn

log2n = log29 ∗ log9n

image-20220819190122851

image-20220819190556494

算法优化方向

尽量少的存储空间

尽量少的执行步骤

空间换时间

时间换空间

多个数据规模

    public static void  test(int n, int k ){
        for (int i = 0; i < n; i++) {
            System.out.println("test");
        }
        for (int i = 0; i < k; i++) {
            System.out.println("test");
        }
    }

补充

一般O(n)计算方法: 
用常数1取代运行时间中所有加法常数。 
在修改后的运行次数函数中,只保留最高阶项。 
如果最高阶项系数存在且不是1,则去除与这个项相乘的常数。
空间复杂度:函数中创建对象的个数关于问题规模函数表达式,一般情况下也用O渐进表示法表示。

递归算法的空间复杂度:递归的深度*递归创建空间的大小。

什么是数据结构

数据结构使计算机存储,组织数据的方式

线性结构:行星表,数组链表栈队列,哈希表

树形结构:AVL树,红黑树,B树,堆,Trie,哈夫曼树,并查集

图形结构:邻接矩阵,邻接表

image-20220819191957569

数组

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

int [] array = new int[] {11,22,33}

动态数组接口设计

◼ int size(); // 元素的数量
◼ boolean isEmpty(); // 是否为空
◼ boolean contains(E element); // 是否包含某个元素
◼ void add(E element); // 添加元素到最后面
◼ E get(int index); // 返回index位置对应的元素
◼ E set(int index, E element); // 设置index位置的元素
◼ void add(int index, E element); // 往index位置添加元素
◼ E remove(int index); // 删除index位置对应的元素
◼ int indexOf(E element); // 查看元素的位置
◼ void clear(); // 清除所有元素

动态数组设计

image-20220819192241787

成员变量会自动初始化

int 类型自动初始化 0

对象初始化null

添加元素add

image-20220819192328892

打印数组

重写toString

在toString 方法中将元素拼接成字符串

字符串拼接建议使用StringBuilder

删除元素

image-20220819192933417

添加元素

image-20220819192956316

数组扩容

import java.util.Arrays;
 
public class CopyArrayDemo {
 
	public static void main(String[] args) {
		int[] arr = { 1, 2, 3, 4, 5 };
		// 数组扩容(一)
		// int[] arr = {1,2,3,4,5}; //数组arr的下标分别为:0 1 2 3 4
		int[] arr_new = new int[6]; // 数组arr_new的下标分别为:0 1 2 3 4 5
		for (int i = 0; i < arr.length; i++) {    //遍历一遍,将数组arr的值传入数组arr_new中
			arr_new[i] = arr[i];    
		}
		// 为新数组赋值
		arr_new[arr_new.length - 1] = 6;
		for (int a : arr_new) {
			System.out.print(a + " ");
		}
		System.out.println();                //换行
 
		// 数组扩容(二)
		// 第一个参数是拷贝的数组,第二个参数扩容长度,并且返回一个新的数组
		int[] copyOf = Arrays.copyOf(arr, arr.length * 2);
		for (int a : copyOf) {
			System.out.print(a + " ");
		}
		System.out.println();                //换行
		
		// 数组扩容(三)
		int arr2[] = { 10, 20, 30, 40, 50, 60, 70, 80, 90 };
		/*
		 * src:表示复制的源数组
		 * srcPos:代表从源数组哪一个元素开始复制(下标)
		 * dest:目标数组
		 * destPos:从目标数组的哪一个元素开始粘贴源数组的数据
		 * length:复制源数组的长度是多少
		 */
		System.arraycopy(arr, 1, arr2, 3, arr.length - 1);
		for (int a : arr2) {
			System.out.print(a + " ");
		}
	}
 
}

泛型

使用泛型技术让动态数组更加通用,可以存放任何数据类型

  public class ArrayList<E>{
        private int size;
        private E[] elements;


    }
    elements = (E[]) new Object[capcity];
    ArrayList<Interger> list = new ArrayList<>();
    

对象数组

image-20220819194415045

链表

动态数组有个明显的缺点

可能会造成内存空间大量浪费

链表可以办到用多少就申请多少内存

链表是一个链式存储线性表,所有元素内存地址不一定连续

image-20220819200135049

接口设计

image-20220819200406166

清空元素

image-20220819200428523

添加元素

image-20220819200447684

node方法用于获取index位置节点

    private Node<E> node(int index){
        rangeCheck(index)
        Node<E>node = first;
        for (int i = 0; i < index; i++) {
            node = node.next;
        }
        return node
    }

添加元素

public void add(int index,E element){
    rangeCheck(index)
    if(index==0){
    first = new Node<element,first>;
    }else{
    Node<E>prev = node(index-1);
    prev.next = new Node<>(element,prev.next)
    }
}

删除元素

image-20220819201902942

反转一个列表

image-20220819202121771

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

向下取整:只取前面的整数

4.1 4
4

向上取整:如果小数不为0,前面的整数加1,小数为0,只取整数

4.5  5 
4.0  4

数组随机访问

随机访问速度快

elements[n]的效率与n是多少无关

动态数组链表复杂度分析

image-20220821141531120

add

最好 O(1)
最坏O(n)
平均O(1)
均摊O(1)

image-20220821141603856

问:什么情况下适合使用均摊复杂度

经过连续多次复杂度比较低的情况后,出现个别复杂度比较高

动态数组缩容

如果内存比较紧张,动态数组比较多的剩余空间可以考虑进行缩容操作

如果扩容倍数,缩容时机设计不当,有可能导致复杂度振荡

双向链表

使用双向链表可以提升链表综合性能

image-20220821141950031

双向链表node法

image-20220821142119519

双向链表add

image-20220821142544027

image-20220821142557301

remove

image-20220821143132886

双向链表 vs 动态数组
◼ 动态数组:开辟、销毁内存空间的次数相对较少,但可能造成内存空间浪费(可以通过缩容解决)
◼ 双向链表:开辟、销毁内存空间的次数相对较多,但不会造成内存空间的浪费
◼ 如果频繁在尾部进行添加、删除操作,动态数组、双向链表均可选择
◼ 如果频繁在头部进行添加、删除操作,建议选择使用双向链表
◼ 如果有频繁的(在任意位置)添加、删除操作,建议选择使用双向链表
◼ 如果有频繁的查询操作(随机访问操作),建议选择使用动态数组
◼ 有了双向链表,单向链表是否就没有任何用处了?
并非如此,在哈希表的设计中就用到了单链表
至于原因,在哈希表章节中会讲到

单向循环列表

image-20220821143625335

单向add

单向remove

双向循环列表

如何发挥循环列表最大威力

可以考虑1个成员变量,3个方法

current :用于指向某个节点

void reset() 让current 指向头节点 first

E next 让 current 往后走一步,也就是 current = current.next

E remove() :删除 current 指向的节点,删除成功后让 current 指向下一个节点

静态列表

前面所学习的链表,是依赖于指针(引用)实现的
◼ 有些编程语言是没有指针的,比如早期的 BASIC、FORTRAN 语言
◼ 没有指针的情况下,如何实现链表?
可以通过数组来模拟链表,称为静态链表
数组的每个元素存放 2 个数据:值、下个元素的索引
数组 0 位置存放的是头结点信息
思考:如果数组的每个元素只能存放 1 个数据呢?
那就使用 2 个数组,1 个数组存放索引关系,1 个数组存放值

栈是一种特殊的线性表,只能在一端进行操作

往栈中添加元素操作,push 入栈

从栈中移除元素操作

pop 出栈,移除栈顶元素

后进先出

image-20220821144605459

栈接口

int size() 元素数量

boolean isEmpty()是否是空

void push(E element)入栈

E top();获取栈顶元素

void clear() 清空

栈 括号面试题

image-20220821145052349

判断括号的有效性可以使用「栈」这一数据结构来解决。

我们遍历给定的字符串 s。当我们遇到一个左括号时,我们会期望在后续的遍历中,有一个相同类型的右括号将其闭合。由于后遇到的左括号要先闭合,因此我们可以将这个左括号放入栈顶。

当我们遇到一个右括号时,我们需要将一个相同类型的左括号闭合。此时,我们可以取出栈顶的左括号并判断它们是否是相同类型的括号。如果不是相同的类型,或者栈中并没有左括号,那么字符串 s 无效,返回 False。为了快速判断括号的类型,我们可以使用哈希表存储每一种括号。哈希表的键为右括号,值为相同类型的左括号。

在遍历结束后,如果栈中没有左括号,说明我们将字符串 s 中的所有左括号闭合,返回 True,否则返回 False。

注意到有效字符串的长度一定为偶数,因此如果字符串的长度为奇数,我们可以直接返回 \text{False}False,省去后续的遍历判断过程。


class Solution {
    public boolean isValid(String s) {
        int n = s.length();
        if (n % 2 == 1) {
            return false;
        }

        Map<Character, Character> pairs = new HashMap<Character, Character>() {{
            put(')', '(');
            put(']', '[');
            put('}', '{');
        }};
        Deque<Character> stack = new LinkedList<Character>();
        for (int i = 0; i < n; i++) {
            char ch = s.charAt(i);
            if (pairs.containsKey(ch)) {
                if (stack.isEmpty() || stack.peek() != pairs.get(ch)) {
                    return false;
                }
                stack.pop();
            } else {
                stack.push(ch);
            }
        }
        return stack.isEmpty();
    }
}


时间复杂度:O(n)O(n),其中 nn 是字符串 ss 的长度。

空间复杂度:O(n+∣Σ∣),其中 Σ 表示字符集,本题中字符串只包含 66 种括号,∣Σ∣=6。栈中的字符数量为 O(n),而哈希表使用的空间为 O((∣Σ∣),相加即可得到总空间复杂度。


有效的括号

遇见左字符,将左字符入栈

如果栈是空的,说明括号无效

如果栈不是空的将栈顶字符出栈,和右字符匹配

如果左右字符不配,说明括号无效

如果左右字符匹配,继续扫描下一个字符

所有字符扫描完毕后

栈是空,说明括号有效

栈不是空,说明括号无效

队列

队列是特殊线性表,只能在头尾两端进行操作

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

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

先进先出原则

队列接口设计

int size() 元素数量

boolean isEmpty()

void clear()

void enQueue (E element)

E deQueue() 出队

E front() 获取队列头元素

动态数组、链表

优先使用双向链表,因为队列主要是往头尾操作元素

用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

实现 MyQueue 类:

void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false
说明:

你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
 


private int front;

public void push(int x) {
    if (s1.empty())
        front = x;
    while (!s1.isEmpty())
        s2.push(s1.pop());
    s2.push(x);
    while (!s2.isEmpty())
        s1.push(s2.pop());
}


复杂度分析

时间复杂度:O(n)O(n)
对于除了新元素之外的所有元素,它们都会被压入两次,弹出两次。新元素只被压入一次,弹出一次。这个过程产生了 4n + 24n+2 次操作,其中 nn 是队列的大小。由于 压入 操作和 弹出 操作的时间复杂度为 O(1)O(1), 所以时间复杂度为 O(n)O(n)。

空间复杂度:O(n)O(n)
需要额外的内存来存储队列中的元素。


双端队列

双端队列是在头尾两端添加,删除队列

int size()
boolean isEmpty()
◼ void clear(); // 清空
◼ void enQueueRear(E element); // 从队尾入队
◼ E deQueueFront(); // 从队头出队
◼ void enQueueFront(E element); // 从队头入队
◼ E deQueueRear(); // 从队尾出队
◼ E front(); // 获取队列的头元素
◼ E rear(); // 获取队列的尾元素

循环队列底层使用动态数组实现,并且各项接口也可以优化到O(1)

时间复杂度

◼ 这个用数组实现并且优化之后的队列也叫做:循环队列

循环队列

image-20220821153259172

循环双端队列

image-20220821153318954

%运算符优化

尽量避免使用乘*、除/、模%、浮点数运算,效率低下

image-20220821153353136

二叉树

节点根节点,父节点,子节点,兄弟节点

节点的度:子树的个数

树的度:所有节点度中最大值

叶子节点:度是0节点

非叶子节点:度不为0的节点

层数:根节点在1层,根节点子节点在2层

节点深度:从根节点到当前节点唯一路径上节点总数

节点的高度:从当前节点到最远叶子节点路径上节点总数

树的深度:所有节点深度最大值

树的高度:所有节点高度最大值

树的深度等于树的高度

有序树,无序树,森林

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

二叉树特点

每个节点度最大是2

左子树和右子树有顺序

即使是某节点只有一个子树,也要区分左右子树

非空二叉树的第 i 层,最多有 2i-1 个节点( i ≥ 1 )

在高度为 h 的二叉树上最多有 2h-1个结点( h ≥ 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

真二叉树

所有节点度都要么是0,要么是2

image-20220821160452951

满二叉树

满二叉树:所有节点的度都要么为 0,要么为 2。且所有的叶子节点都在最后一层

image-20220821160507065

假设满二叉树的高度为 h( h ≥ 1 ),那么 第 i 层的节点数量 2i-1

叶子节点数量2h-1

✓n = 2 h − 1 = 2 0 + 2 1 + 2 2 + ⋯ + 2 h−1

h = log2(n + 1)

完全二叉树

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

image-20220821163750789

完全二叉树从根节点到倒数第二层是一个满二叉树

满二叉树一定时完全二叉树,完全二叉树不一定是满二叉树

性质

度为1的节点只有左子树

度是1的节点要么是1个要么是0个

同样是节点数量的二叉树,完全二叉树高度最小

image-20220821163951589

一棵有 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 ,它无右子节点
一棵有 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,度为 1 的节点个数为 n1,度为 2 的节点个数为 n2
总结点个数 n = n0 + n1 + n2,而且 n0 = n2 + 1
✓ n = 2n0 + n1 – 1
完全二叉树的 n1 要么为 0,要么为 1
✓ n1为1时,n = 2n0,n 必然是偶数
➢ 叶子节点个数 n0 = n / 2,非叶子节点个数 n1 + n2 = n / 2
✓ n1为0时,n = 2n0 – 1,n 必然是奇数
➢ 叶子节点个数 n0 = (n + 1) / 2,非叶子节点个数 n1 + n2 = (n – 1) / 2
叶子节点个数 n0 = floor( (n + 1) / 2 ) = ceiling( n / 2 ) 
非叶子节点个数 n1 + n2 = floor( n / 2 ) = ceiling( (n – 1) / 2 ) 
因此叶子节点个数为 384

二叉树遍历

把所有元素都访问一遍

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

正序遍历

逆序遍历

前序

中序

后序

层序

前序遍历

image-20220821171244422

先访问根节点,前序遍历左子树,前序遍历右子树

中序遍历

image-20220821171255422

左子树 ,根节点,右子树

后序遍历

左子树,右子树,根节点

image-20220821171307845

层序遍历

从上到下,从左到右依次遍历

image-20220821172923968

◼ 实现思路:使用队列
1. 将根节点入队
2. 循环执行以下操作,直到队列为空
将队头节点 A 出队,进行访问
将 A 的左子节点入队
将 A 的右子节点入队
/**
	 * 前序遍历
	 */
	public void preorderTraversal() {
		preorderTraversal(root);
	}

	private void preorderTraversal(Node<E> node) {
		if (node == null) return;

		System.out.println(node.element);
		preorderTraversal(node.left);
		preorderTraversal(node.right);
	}

	/**
	 * 中序遍历
	 */
	public void inorderTraversal() {
		inorderTraversal(root);
	}

	private void inorderTraversal(Node<E> node) {
		if (node == null) return;

		inorderTraversal(node.left);
		System.out.println(node.element);
		inorderTraversal(node.right);
	}

	/**
	 * 后序遍历
	 */
	public void postorderTraversal() {
		postorderTraversal(root);
	}

	private void postorderTraversal(Node<E> node) {
		if (node == null) return;

		postorderTraversal(node.left);
		postorderTraversal(node.right);
		System.out.println(node.element);
	}

	/**
	 * 层序遍历
	 */
	public void levelOrderTraversal() {
		if (root == null) return;

		Queue<Node<E>> queue = new LinkedList<>();
		queue.offer(root);

		while (!queue.isEmpty()) {
			Node<E> node = queue.poll();
			System.out.println(node.element);

			if (node.left != null) {
				queue.offer(node.left);
			}

			if (node.right != null) {
				queue.offer(node.right);
			}
		}
	}
 public static abstract class Visitor<E> {
        boolean stop;
        /**
         * @return 如果返回true,就代表停止遍历
         */
        public abstract boolean visit(E element);
    }



public void preorder(Visitor<E> visitor) {
        if (visitor == null) return;
        preorder(root, visitor);
    }

    private void preorder(Node<E> node, Visitor<E> visitor) {
        if (node == null || visitor.stop) return;

        visitor.stop = visitor.visit(node.element);
        preorder(node.left, visitor);
        preorder(node.right, visitor);
    }

    public void inorder(Visitor<E> visitor) {
        if (visitor == null) return;
        inorder(root, visitor);
    }

    private void inorder(Node<E> node, Visitor<E> visitor) {
        if (node == null || visitor.stop) return;

        inorder(node.left, visitor);
        if (visitor.stop) return;
        visitor.stop = visitor.visit(node.element);
        inorder(node.right, visitor);
    }

    public void postorder(Visitor<E> visitor) {
        if (visitor == null) return;
        postorder(root, visitor);
    }

    private void postorder(Node<E> node, Visitor<E> visitor) {
        if (node == null || visitor.stop) return;

        postorder(node.left, visitor);
        postorder(node.right, visitor);
        if (visitor.stop) return;
        visitor.stop = visitor.visit(node.element);
    }

优化

    public void levelOrder(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();
            if (visitor.visit(node.element)) return;

            if (node.left != null) {
                queue.offer(node.left);
            }

            if (node.right != null) {
                queue.offer(node.right);
            }
        }
    }

树状打印二叉树

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        toString(root, sb, "");
        return sb.toString();
    }

    private void toString(Node<E> node, StringBuilder sb, String prefix) {
        if (node == null) return;

        toString(node.left, sb, prefix + "L---");
        sb.append(prefix).append(node.element).append("\n");
        toString(node.right, sb, prefix + "R---");
    }

完全二叉树判断

    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;
    }

计算二叉树的高度

  public int height() {
        if (root == null) return 0;

        // 树的高度
        int height = 0;
        // 存储着每一层的元素数量
        int levelSize = 1;
        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            Node<E> node = queue.poll();
            levelSize--;

            if (node.left != null) {
                queue.offer(node.left);
            }

            if (node.right != null) {
                queue.offer(node.right);
            }

            if (levelSize == 0) { // 意味着即将要访问下一层
                levelSize = queue.size();
                height++;
            }
        }

        return height;
    }

    public int height2() {
        return height(root);
    }

    private int height(Node<E> node) {
        if (node == null) return 0;
        return 1 + Math.max(height(node.left), height(node.right));
    }

反转二叉树

package datastruct.tree.翻转二叉树;

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

public class invertTree {


    // 首先我们考虑的不是用什么方式遍历的问题,而是考虑题目想要干什么
    public TreeNode invertTree1(TreeNode root){
        if (root == null) return root;

        TreeNode tmp = root.left;
        root.left = root.right;
        root.right = tmp;

        invertTree1(root.left);
        invertTree1(root.right);
        return root;
    }

//   public TreeNode invertTree(TreeNode root) {
//	   if (root == null) return root;
//
//	   TreeNode tmp = root.left;
//	   root.left = root.right;
//	   root.right = tmp;
//
//       invertTree(root.left);
//       invertTree(root.right);
//
//       return root;
//   }

//	public TreeNode invertTree(TreeNode root) {
//	   if (root == null) return root;
//
//       invertTree(root.left);
//       invertTree(root.right);
//
//	   TreeNode tmp = root.left;
//	   root.left = root.right;
//	   root.right = tmp;
//
//       return root;
//    }

//	public TreeNode invertTree(TreeNode root) {
//	   if (root == null) return root;
//
//       invertTree(root.left);
//
//	   TreeNode tmp = root.left;
//	   root.left = root.right;
//	   root.right = tmp;
//
//       invertTree(root.left);
//
//       return root;
//    }

    // 层序遍历
    public TreeNode invertTree(TreeNode root) {
        if (root == null) return root;

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();

            TreeNode tmp = node.left;
            node.left = node.right;
            node.right = tmp;

            if (node.left != null) {
                queue.offer(node.left);
            }

            if (node.right != null) {
                queue.offer(node.right);
            }
        }
        return root;
    }


}

遍历应用

前序遍历

树状结构展示

中序遍历

二叉搜索树中序遍历按升序或者降序处理节点

后序遍历

先子后父

层序遍历

计算二叉树的高度

根据遍历结果重构二叉树

前序遍历 + 中序遍历

后序遍历 + 中序遍历

前序遍历 + 后序遍历

image-20220822153822276

前+中

◼ 前序遍历:4 2 1 3 6 5 ◼ 中序遍历:1 2 3 4 5 6

前驱节点和后继节点

 private Node<E> predecessor(Node<E> node) {
        if (node == null) return null;

        // 前驱节点在左子树当中(left.right.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;
    }

    private Node<E> successor(Node<E> node) {
        if (node == null) return null;

        // 前驱节点在左子树当中(right.left.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;
        }

        return node.parent;
    }

二叉搜索树

在 n 个动态的整数中搜索某个整数?(查看其是否存在)
◼ 针对这个需求,有没有更好的方案?
使用二叉搜索树,添加、删除、搜索的最坏时间复杂度均可优化至:O(logn)
0 1 2 3 4 5 6 7 8 9
31 66 17 15 28 20 59 88 45 56
◼ 如果维护一个有序的动态数组,使用二分搜索,最坏时间复杂度:O(logn)
但是添加、删除的平均时间复杂度是 O(n)
0 1 2 3 4 5 6 7 8 9
15 17 20 28 31 45 56 59 66 88
◼ 假设使用动态数组存放元素,从第 0 个位置开始遍历搜索,平均时间复杂度:O(n)

删除节点

删除度为0的节点,删除叶子节点

node == node.parent.left
✓ node.parent.left = null
node == node.parent.right
✓ node.parent.right = null
node.parent == null
✓ root = null

image-20220822201622983

删除度为1的节点

用子节点替代源节点的位置

image-20220822202357976

child 是 node.left 或者 child 是 node.right

用 child 替代 node 的位置
✓ 如果 node 是左子节点
➢ child.parent = node.parent
➢ node.parent.left = child

✓ 如果 node 是右子节点
➢ child.parent = node.parent
➢ node.parent.right = child

✓ 如果 node 是根节点
➢ root = child
➢ child.parent = null

删除度为2的节点(删除根节点)

◼ 举例:先删除 5、再删除 4
◼ 先用前驱或者后继节点的值覆盖原节点的值
◼ 然后删除相应的前驱或者后继节点
◼ 如果一个节点的度为 2,那么
它的前驱、后继节点的度只可能是 1 和 0

image-20220822202410445

 private void remove(Node<E> node) {
        if (node == null) return;

        size--;

        if (node.hasTwoChildren()) { // 度为2的节点
            // 找到后继节点
            Node<E> s = successor(node);
            // 用后继节点的值覆盖度为2的节点的值
            node.element = s.element;
            // 删除后继节点
            node = s;
        }

        // 删除node节点(node的度必然是1或者0)
        Node<E> 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 == node.parent.right
                node.parent.right = replacement;
            }
        } else if (node.parent == null) { // node是叶子节点并且是根节点
            root = null;
        } else { // node是叶子节点,但不是根节点
            if (node == node.parent.left) {
                node.parent.left = null;
            } else { // node == node.parent.right
                node.parent.right = null;
            }
        }
    }


------------------------------
找后继节点代码
    private Node<E> successor(Node<E> node) {
        if (node == null) return null;

        // 前驱节点在左子树当中(right.left.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;
        }

        return node.parent;
    }

--------------
node

 private 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 { // cmp < 0
                node = node.left;
            }
        }
        return null;
    }

这篇关于数据结构(java版)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!