数据结构与算法
一、数据结构
1.1、数据结构基本属性
程序 = 数据结构 + 算法
- 数据元素是数据的基本单元,数据元素也称结点
- 逻辑结构:数据元素之间的逻辑关系
- 数据存储结构:数据元素与存储之间的关系
- 集合
- 数据元素都属于同一个集合,且数据元素之间没有任何关系
- 线性结构
- 线性结构是一种前后关联的结构体,数据元素之间存在着一对一的关系
- 树形结构
- 一个结点下面可以连接多个子节点,也称孩子结点,数据元素之间存在着一对多的关系
- 图形结构
- 图形结构也称网状结构,多个结点之间相互连接,数据元素之间存在着多对多阿关系
1.2、数据结构存储结构
- 数据逻辑结构和存储结构的关系:
- 存储结构是逻辑关系的映像和元素的映像,是数据结构的实现
- 逻辑结构是数据结构的抽象表达方式
- 存储结构的分类
- 顺序结构
- 作用于内存相对位置表示数据元素的逻辑结构
- 一般便于查询和随机访问数据元素
- 不利于数据的修改、删除、插入和移动
- 链式结构
- 作用于存储地址的指针表示数据元素的逻辑关系
- 一般便于数据的修改、删除、插入和移动
- 不能对元素进行随机访问和查询
- 索引结构
- 在原有的存储结构上加索引表,索引表由关键字和存储地址组成
- 提高随机访问及查询效率
- 散列结构
- 提高构造函数来确定数据存储地址或查询地址
- 提高随机访问及查询效率
- 顺序结构
1.3、算法简介
- 算法的特征
- 确定性,计算或预测的数据指令一定是确切的,不会产生异议性
- 可行性,算法可以实现逻辑或问题的真实处理
- 有穷性,必须要在有限的时间和空间完成并输出结果
- 输入/输出,可以有零个或多个输入/输出
- 算法的目的
- 正确性,能够正确执行预先设定好的功能达到预期要求
- 可读性,便于理解、修改、拓展
- 健壮性,执行过程中不会因自身或外界元素而打断执行或出现意外错误
- 高效性,算法一定要高效才符合程序设计的初心
- 低存储量,算法体量不宜臃肿,不能再执行后增加存储
二、探究复杂度
2.1、斐波那契数列
0 | 0 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 33 | …. |
---|---|---|---|---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | …. |
- 斐波那契数列简单理解,第三项 = 前一项 + 前二项
实现方法
方法一
//方法一实现斐波那契数
public static int fib(int n){
//要考虑n=0、n=1的情况
if(n<=1){
return n;
}
//第三项=前一项+前二项
return fib(n-1)+fib(n-2);
}
/*
*复杂度=n^2
*/
@Test
public void fibTest(){
System.out.println("n=3:"+fib(3));
System.out.println("n=20:"+fib(20));
System.out.println("n=40:"+fib(40));
}
方法二
//方法二实现斐波那契数
public static int fib2(int n){
//要考虑n=0、n=1的情况
if(n<=1){
return n;
}
int fisrt = 0;
int second = 1;
//
for (int i = 0; i < n-1 ; i++) {
int sum = fisrt + second;
//每次执行的结果都是第二次执行的首项
fisrt =second;
//每次执行的第二项都是第一次执行的结果
second = sum;
}
return second;
}
/*
* 时间复杂度=n
*/
@Test
public void fib2Test(){
System.out.println("n=30:"+fib2(30));
System.out.println("n=60:"+fib2(60));
System.out.println("n=90:"+fib2(90));
}
时间计算 ```java //时间计算类 public class TimeTool { private static final SimpleDateFormat fmt = new SimpleDateFormat(“HH:mm:ss.SSSS”);
public interface Task{ void execute(); }
public static void check(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(“————————————“);
} }
//测试 @Test public void timeTest(){ int n=46; TimeTool.check(“fib”, new TimeTool.Task() { @Override public void execute() { System.out.println(fib(n)); } });
TimeTool.check("fib2", new TimeTool.Task() {
@Override
public void execute() {
System.out.println(fib2(n));
}
});
}
<br />![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210609215046296.png#crop=0&crop=0&crop=1&crop=1&id=oAsTQ&originHeight=379&originWidth=724&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- **特征方程解决斐波那契数列**<br />![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210610192515990.png#crop=0&crop=0&crop=1&crop=1&id=XkzfJ&originHeight=437&originWidth=1239&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="31297a97"></a>
### 2.2、时间复杂度
- 时间复杂度是衡量一个程序性能好坏的重要指标之一,也是衡量一个算法好坏的重要指标之一
- 时间复杂度的计算
- **一个分号执行1次 O(1)**
- for循环执行次数取决于n值大小
- **单层for循环一般为3n次 O(n)**
- **双重for循环一般为9n^2+6n+1 O(n^2)**
- **n = m /2的情况 Ologm(n)——>Ologn**
- **时间复杂度大小对比**
- **O(1) < O(logn) < O(n) < O(n^2) < O(n^3) < O(2^n) <O(n!) < O(n^n)** ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210609230526918.png#crop=0&crop=0&crop=1&crop=1&id=vsk2v&originHeight=656&originWidth=1099&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="f9608952"></a>
### 2.3、空间复杂读
- 算法对内存资源的消耗,一般运行不消耗内存不会增加就算优质算法了
- 在程序设计中,一个优质的算法 = 时间复杂度 + 空间复杂度
- 在解决算法调优时,一般选择以空间复杂度换时间复杂度
<a name="f91df17a"></a>
## 三、线性数据结构
<a name="38fdfce3"></a>
#### 3.1、线性表
<a name="c5bb71ee"></a>
##### 3.1.1、什么是线性表
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210610145733469.png#crop=0&crop=0&crop=1&crop=1&id=dzPBy&originHeight=358&originWidth=1429&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 线性表是一种线性结构,它是由零个或多个数据元素构成的**有限序列**。
- 线性表的特征是在一个序列中,除了头尾元素,每个元素都有且**只有一个直接前驱**,**有且只有一个直接后继**,而序列头元素没有直接前驱,序列尾元素没有直接后继。
- 线性表中的元素是相同的抽象数据类型。
<a name="1e604ff8"></a>
##### 3.1.2、线性表的分类
- 常用的数组、链表、栈、队列等都是线性结构的数据结构
- **数组中的战斗机**
- **vector和array、数组**
- 相同点:
- 三者均可以使用下表运算符对元素进行操作,即vector和array都针对下标运算符[]进行了重载
- 三者在内存的方面都使用连续内存,即在vector和array的底层存储结构均使用数组
- 都在java.util包下
- 不同点:
- vector属于变长容器,即可以根据数据的插入删除重新构建容器容量(1.5倍或者2倍);但array和数组属于定长容量
- vector和array提供了更好的数据访问机制,即可以使用front和back以及at访问方式,使得访问更加安全。
- vector和array提供了更好的遍历机制,即有正向迭代器和反向迭代器两种
- array提供了初始化所有成员的方法fill
- vector提供了可以动态插入和删除元素的机制,而array和数组则无法做到,或者说array和数组需要完成该功能则需要自己实现完成
- **vector和arrayList**
- vector是线程同步(Synchronized)的,所以它也是线程安全的
- Arraylist是线程异步(ASynchronized)的,是不安全的
- 正因vetor线程同步,所以在执行中多了获取锁资源的过程,效率比arrayList慢
- ArrayList和Vector都采用线性连续存储空间,当存储空间不足的时候,ArrayList默认增加为原来的50%,Vector默认增加为原来的一倍;
<a name="04dc329f"></a>
#### 3.2、动态数组
<a name="1fe7f3ed"></a>
##### 3.2.1、深入了解数组
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210610145908686.png#crop=0&crop=0&crop=1&crop=1&id=knmyR&originHeight=400&originWidth=562&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- `int[] array = new int []{}`,向栈空间申请一段可使用内存放在int类型的array数组里
- 数组无法动态修改容量,创建时申请多少,使用过程中就是多少
<a name="2ac6bc8d"></a>
##### 3.2.2、动态数组
- **接口参数设计** ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210610151650652.png#crop=0&crop=0&crop=1&crop=1&id=Jne9C&originHeight=299&originWidth=557&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- **对象数组**
- 数组元素可以是任何类型(只要所有元素具有相同的类型)
- 数组元素可以是基本数据类型
- 数组元素也可以是类对象,称这样的数组为对象数组。在这种情况下,数组的每一个元素都是一个对象的引用。
- 先申请堆内存,再创建数组对象,可以减少一定程度的内存资源浪费
```java
//创建数组对象,申请堆内存空间
Object[i] list = new Object[10];
//创建数组对象
object[0] = new Person(1,"java");
object[1] = new Person(2,"python");
object[2] = new Person(3,"C++");
- 数组扩容
- 按倍数增加,但存在线程风险且效率不高
//数组扩容处理
private void ensureCapacity(int capcity){
//定义为扩容前的数组长度
int oldCapacity =elements.length;
//判断容量大小
if(oldCapacity >= capcity){
return;
}
//定义数组扩容的大小,1+0.5
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
elements = newElements;
System.out.println(oldCapacity+"扩容:"+newCapacity);
}
- 按倍数增加,但存在线程风险且效率不高
- 系统内置函数,成倍增加
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
//src 原始数组
//srcPos 原始数组中的起始位置
//dest 目标数组
//destPos 目标数据中的起始位置
//length 要复制的数组元素的数量
3.2.3、动态数组的实现
/**
* @author hguo
* @date2021/6/10 15:14
* @title 动态数组接口设计
*/
public class ArrayList<E>{
private int size;
private E[] elements;
/**
* 数组的默认容量
*/
private static final int DEFAULT_CAPACITY = 5;
/**
* 找不到元素返回-1
*/
private static final int ELEMENT_NOT_FOUND = -1;
public ArrayList(int capacity) {
// 设置默认容量为 10
capacity = Math.max(capacity, DEFAULT_CAPACITY);
// 因为泛型(所以传一个Object数组,然后通过强转)
elements = (E[]) new Object[capacity];
}
public ArrayList() {
// 默认容量
this(DEFAULT_CAPACITY); // 调用下面的构造器
}
//数组越界,异常处理
private void OutOfBounds(int index) {
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size+":数组越界,请正确输入!");
}
//限定CRUD操作范围
private void rangeCheck(int index) {
if (index < 0 || index >= size) {
OutOfBounds(index);
}
}
//插入操作的范围限定
private void rangeCheckForAdd(int index) {
if (index < 0 || index > size) {
OutOfBounds(index);
}
}
//数组扩容处理
private void ensureCapacity(int capcity){
//定义为扩容前的数组长度
int oldCapacity =elements.length;
//判断容量大小
if(oldCapacity >= capcity){
return;
}
//定义数组扩容的大小,1+0.5
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
elements = newElements;
System.out.println(oldCapacity+"扩容:"+newCapacity);
}
/**
* 数组缩容
*/
public void trim() {
// 获取当前数组的容量
int oldcapacity = elements.length;
// 计算新的容量 = 原有容量的一半
int newCapacity = oldcapacity >> 1;
// 当size大于等于容量的一半, 或则容量已经小于默认容量(10)时, 直接返回
if (size >= newCapacity >> 1 || oldcapacity < DEFAULT_CAPACITY) return;
// 创建新数组
E[] newElements = (E[]) new Object[newCapacity];
// 将原数组元素存入新数组
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
// 引用新数组
elements = newElements;
System.out.println(oldcapacity+"缩容:"+newCapacity);
}
// 元素的数量
public int size() {
return size;
}
// 是否为空
public boolean isEmpty() {
return size == 0;
}
// 是否包含某个元素
public boolean contains(E element) {
return indexOf(element)!= ELEMENT_NOT_FOUND;
}
// 添加元素到最后面
public void add(E element) {
add(size,element);
}
// 返回index位置对应的元素
public E get(int index) {
rangeCheck(index);
return elements[index];
}
// 设置index位置的元素
public E set(int index, E element) {
rangeCheck(index);
E old = elements[index];
elements[index] = element;
return old;
}
// 往index位置添加元素
public void add(int index, E element) {
//插入值为空时
if(element == null){
return;
}
//处理数组越界
rangeCheckForAdd(index);
//数组扩容
ensureCapacity(size+1);
//从后往前开始操作
for (int i = size - 1; i >=index ; i--) {
elements[i + 1] =elements[i];
}
elements[index] = element;
size++;
}
// 删除index位置对应的元素
public E remove(int index) {
rangeCheck(index);
E old = elements[index];
// 当删除一个元素时,需要挪动后面元素的范围
for (int i = index + 1; i <= size - 1; i++) {
elements[i - 1] = elements[i];
}
// 同clear的细节,当从后往前以后时,最后一个的地址需要释放
//清空最后一个元素,先把位置减一再清空
elements[--size] = null;
//缩容操作
trim();
return old;
}
// 查看元素的位置
public int indexOf(E element) {
for (int i = 0; i < size; i++) {
// 因为element肯定不为null了,所以放在前面;避免空指针异常
if (elements[i].equals(element)) //比较数组即可,不用比较内存地址
{
return i;
}
}
return ELEMENT_NOT_FOUND;
}
// 清除所有元素
public void clear() {
for (int i = 0; i <size; i++) {
//清空数组
elements[i] = null;
}
size = 0;
}
@Override
public String toString() {
//使用StringBuilder进行字符串拼接
StringBuilder string = new StringBuilder();
//定义拼接格式
string.append("size=").append(size).append(",[");
//循环遍历数组内容
for (int i = 0; i < size; i++) {
if(i != 0 ){
string.append(",");
}
string.append(elements[i]);
//元素用,隔开,但要判断元素是否为最后一个,最后一个不破解分隔符
}
string.append("]");
//打印数组内容
return string.toString();
}
}
扩容操作 ```java
//数组扩容处理 private void ensureCapacity(int capcity){
//定义为扩容前的数组长度
int oldCapacity =elements.length;
//判断容量大小
if(oldCapacity >= capcity){
return;
}
//定义数组扩容的大小,1+0.5
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
elements = newElements;
System.out.println(oldCapacity+"扩容:"+newCapacity);
}
// 往index位置添加元素 public void add(int index, E element) { //插入值为空时 if(element == null){ return; } //处理数组越界 rangeCheckForAdd(index); //数组扩容 ensureCapacity(size+1); //从后往前开始操作 for (int i = size - 1; i >=index ; i—) { elements[i + 1] =elements[i]; } elements[index] = element; size++; }
- **测试**
```java
@Test
public void arrayList2() {
ArrayList<Integer> list = new ArrayList<>();
list.add(12);
list.add(13);
list.add(14);
list.add(15);
list.add(16);
list.add(17);
list.add(19);
list.add(20);
list.add(21);
list.add(22);
list.add(5,18);
System.out.println(list);
}
- 缩容操作
```java
/**
- 数组缩容
*/
public void trim() {
// 获取当前数组的容量
int oldcapacity = elements.length;
// 计算新的容量 = 原有容量的一半
int newCapacity = oldcapacity >> 1;
// 当size大于等于容量的一半, 或则容量已经小于默认容量(10)时, 直接返回
if (size >= newCapacity >> 1 || oldcapacity < DEFAULT_CAPACITY) return;
// 创建新数组
E[] newElements = (E[]) new Object[newCapacity];
// 将原数组元素存入新数组
for (int i = 0; i < size; i++) {
} // 引用新数组 elements = newElements; System.out.println(oldcapacity+”缩容:”+newCapacity); }newElements[i] = elements[i];
/**
* 删除index位置的元素
* @param index
* @return 被删除的元素
*/
public E remove(int index) {
rangeCheck(index);
E delEle = elements[index];
// 当删除一个元素时,需要挪动后面元素的范围
for (int i = index + 1; i <= size - 1; i++) {
elements[i - 1] = elements[i];
}
size--;
// 同clear的细节,当从后往前以后时,最后一个的地址需要释放
elements[size] = null;
// 判断数组是否需要缩容
trim();
return delEle;
}
- **测试**
```java
@Test
public void arrayList3(){
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 50; i++) {
list.add(i);
}
for (int i = 0; i < 50; i++) {
list.remove(0);
}
System.out.println(list);
}
3.3、链表
3.3.1、什么链表
- 链表和数组是同一级别的数据结构,链表在进行循环遍历时效率不高,但是插入和删除时优势明显。
- 链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针连接次序实现的
- 链表由一个个子节点构成,每个节点有两个部分:数据域和指针域
- 数据域就是实际存储数据的,指针域可以有一个和两个,单链表就是单个指针域指向后一个节点,双链表就是节点有两个指针域,分别指向前一个和后一个节点。
3.3.2、链表的实现原理
- 创建一个节点类,其中节点类包含两个部分,第一个是数据域(你到时候要往节点里面储存的信息),第二个是引用域(相当于指针,单向链表有一个指针,指向下一个节点;双向链表有两个指针,分别指向下一个和上一个节点)
- 创建一个链表类,其中链表类包含三个属性:头结点、尾节点和大小,方法包含添加、删除、插入等等方法
3.3.3、链表的分类
- 单向链表:链表中的元素的指向只能指向链表中的下一个元素或者为空,元素之间不能相互指向,也就是一种线性链表
- 双向链表:个有序的结点序列,每个链表元素既有指向下一个元素的指针,又有指向前一个元素的指针,其中每个结点都有两种指针,即front和tail,front指针指向左边结点,tail指针指向右边结点
3.3.4、链表接口设计
定义公共接口
/**
* @author hguo
* @date2021/6/11 11:14
* @title List公共接口的设计
*/
public interface List<E> {
public static final int ELEMENT_NOT_FOUND = -1;
/**
* 清除所有元素
*/
public void clear();
/**
* 元素的数量
* @return
*/
public int size();
/**
* 是否为空
* @return
*/
boolean isEmpty();
/**
* 是否包含某个元素
* @param element
* @return
*/
boolean contains(E element);
/**
* 添加元素到尾部
* @param element
*/
void add(E element);
/**
* 获取index位置的元素
* @param index
* @return
*/
E get(int index);
/**
* 设置index位置的元素
* @param index
* @param element
* @return 原来的元素ֵ
*/
E set(int index, E element);
/**
* 在index位置插入一个元素
* @param index
* @param element
*/
void add(int index, E element);
/**
* 删除index位置的元素
* @param index
* @return
*/
E remove(int index);
/**
* 查看元素的索引
* @param element
* @return
*/
int indexOf(E element);
}
定义公共抽象类,使用时,不论是数组还是链表只需要基础该抽象类即可实现定义的功能
/**
* @author hguo
* @date2021/6/11 11:22
* @title 定义公共抽象类
*/
public abstract class AbstractList<E> implements List<E>{
protected int size;
// 下标越界抛出的异常
protected void outOfBounds(int index) {
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size+":数组越界,请正确输入!");
}
// 检查下标越界(不可访问或删除size位置)
protected void rangeCheck(int index){
if(index < 0 || index >= size){
outOfBounds(index);
}
}
// 检查add()的下标越界(可以在size位置添加元素)
protected void rangeCheckForAdd(int index) {
if (index < 0 || index > size) {
outOfBounds(index);
}
}
@Override
public boolean contains(E element) {
return indexOf(element)!=ELEMENT_NOT_FOUND;
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void add(E element) {
add(size, element);
}
}
3.3.5、单向链表的实现
1、单向链表的操作
2、接口设计分析
- 链表和数组接口大致相同,数组为ArrayList,链表为LinkList,因此在设计共用接口时,可以定义一个公共父类接口让数组和链表调用
单向链表一 ```java /**
- @author hguo
- @date2021/6/11 11:26
@title 单链表操作 */ public class SingleLinkedList
extends AbstractList { //定义初始结点 private Node
first; // 链表中的节点 private static class Node
{ E element; // 节点元素 Node next; // 节点指向下一个节点 public Node(E element, Node
next) { this.element = element;
this.next = next;
} }
/**
- 根据索引找到节点对象
- @param index
- @return
*/
private Node
node(int index) { rangeCheck(index); Node node = first; for (int i = 0; i < index; i++) {
} return node; }node = node.next;
@Override public void clear() { // size = 0; first = null; }
@Override public E get(int index) { /*
* 最好:O(1)
* 最坏:O(n)
* 平均:O(n)
*/
return node(index).element; }
@Override
public E set(int index, E element) {
/*
* 最好:O(1)
* 最坏:O(n)
* 平均:O(n)
*/
E old = node(index).element;
node(index).element = element;
return old;
}
@Override
public void add(int index, E element) {
/*
* 最好:O(1)
* 最坏:O(n)
* 平均:O(n)
*/
rangeCheckForAdd(index);
if(index == 0){ // 给空链表添加第一个元素的情况
first = new Node<>(element, first);
}else{
Node<E> prev = node(index - 1);
prev.next = new Node<>(element, prev.next);
}
size++;
}
@Override
public E remove(int index) {
/*
* 最好:O(1)
* 最坏:O(n)
* 平均:O(n)
*/
rangeCheck(index);
Node<E> node = first;
if (index == 0) { // 删除第一个元素是特殊情况
first = first.next;
} else {
Node<E> prev = node(index - 1); // 找到前一个元素
node = prev.next; // 要删除的元素
prev.next = node.next; // 删除元素
}
size--;
return node.element;
}
/**
* 查看元素的位置
* @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 {
for (int i = 0; i < size; i++) {
Node<E> node = first;
// 因为element肯定不为null了,所以放在前面;避免空指针异常
if (node.element.equals(element)) //比较数组即可,不用比较内存地址
{
return i;
}
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
//拼接字符串并打印
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(": [");
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (i != 0) {
string.append(", ");
}
string.append(node.element);
node = node.next;
}
string.append("]");
return string.toString();
}
}
- **清空元素**:`next` 不需要设置为 `null`,因为 `first` 指向了 `null`,后面的 `Node` 没有被指向,在 Java 中会自动被垃圾回收 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210611135921074.png#crop=0&crop=0&crop=1&crop=1&id=s2oql&originHeight=361&originWidth=991&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- **添加元素**:需要考虑链表首位位置的元素,size=0/size=next ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210611140121430.png#crop=0&crop=0&crop=1&crop=1&id=SNHgt&originHeight=492&originWidth=987&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- **删除元素:**需要考虑链表首位位置的元素,size=0/size=next ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210611140212999.png#crop=0&crop=0&crop=1&crop=1&id=fK72f&originHeight=489&originWidth=991&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- **单向链表二(虚拟头结点)**
- **虚拟的头结点(不存储数据)**,减少代码冗余,增加业务处理逻辑 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210611140444599.png#crop=0&crop=0&crop=1&crop=1&id=sXEvP&originHeight=302&originWidth=983&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 整体与单项链表无异,但因为多了虚拟头结点,增加/删除元素的方法有变化(add/remove)
- **创建虚拟头结点**
```java
public SingleLinkedList() {
// 初始化一个虚拟头结点
first = new Node<>(null, null);
};
- 增加方法
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
Node<E> prev = (index == 0) ? first : node(index - 1);
prev.next = new Node<>(element, prev.next);
size++;
}
删除方法
@Override
public E remove(int index) {
rangeCheck(index);
Node<E> prev = (index == 0) ? first : node(index - 1);
Node<E> node = prev.next;
prev.next = node.next;
size--;
return prev.element;
}
- 虚拟头结点单向链表
- 头结点为空,无指向(NULL)(不建议使用) ```java /**
- @author hguo
- @date2021/6/11 14:12
- @title 虚拟头结点单向链表 */
/**
- 增加一个虚拟头结点
@param
*/ public class SingleLinkedList2 extends AbstractList { //定义初始结点 private Node first; /**
- 初始化一个虚拟头结点 */ public SingleLinkedList2() { first = new Node<>(null, null); };
/**
* 链表中的节点
* @param <E>
*/
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 void clear() {
size = 0;
first = null;
}
/**
* 获取元素
* @param index
* @return
*/
@Override
public E get(int index) {
return node(index).element;
}
/**
* 设置或修改元素
* @param index
* @param element
* @return
*/
@Override
public E set(int index, E element) {
E old = node(index).element;
node(index).element = element;
return old;
}
/**
* 插入元素
* @param index
* @param element
*/
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
Node<E> prev = (index == 0) ? first : node(index - 1);
prev.next = new Node<>(element, prev.next);
size++;
}
/**
* 删除元素
* @param index
* @return
*/
@Override
public E remove(int index) {
rangeCheck(index);
Node<E> prev = (index == 0) ? first : node(index - 1);
Node<E> node = prev.next;
prev.next = node.next;
size--;
return prev.element;
}
/**
* 索引元素
* @param element
* @return
*/
@Override
public int indexOf(E element) {
// 有个注意点, 如果传入元素为null, 则不能调用equals方法, 否则会空指针
// 因此需要对元素是否为null做分别处理
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 (node.element.equals(element)) return i;
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
/**
* 根据索引找到节点
* @param index
* @return
*/
private Node<E> node(int index) {
rangeCheck(index);
Node<E> node = first.next;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(": [");
Node<E> node = first.next;
for (int i = 0; i < size; i++) {
if (i != 0) {
string.append(", ");
}
string.append(node.element);
node = node.next;
}
string.append("]");
return string.toString();
}
}
<a name="d6ee3be5"></a>
###### 3、复杂度分析
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210611145222441.png#crop=0&crop=0&crop=1&crop=1&id=zyX5Y&originHeight=315&originWidth=821&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="cc019d10"></a>
##### 3.3.6、双向链表的实现
<a name="b25bb406"></a>
###### 1、双向链表的操作
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210613144527156.png#crop=0&crop=0&crop=1&crop=1&id=A9xzS&originHeight=592&originWidth=975&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210613144636204.png#crop=0&crop=0&crop=1&crop=1&id=Is1Fx&originHeight=509&originWidth=984&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210613144527156.png#crop=0&crop=0&crop=1&crop=1&id=jySlN&originHeight=592&originWidth=975&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210613144636204.png#crop=0&crop=0&crop=1&crop=1&id=F59J8&originHeight=509&originWidth=984&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="f9d466a6"></a>
###### 2、接口设计分析
- 双向链表的出现只为解决单向链表查询或插入只能单向操作导致效率低的问题
- 双链表是链表的一种,由节点组成,**每个数据结点中都有两个指针,分别指向直接后继和直接前驱**。
- 在链表存储多个元素时,对元素的操作可以正向操作,也可以逆向操作,提高了增删改查的结点查询效率 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210611162507109.png#crop=0&crop=0&crop=1&crop=1&id=p3ESO&originHeight=455&originWidth=929&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 双向链表**只有一个元素**的情况:`first`、`last` 指向同一个节点 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210611162651385.png#crop=0&crop=0&crop=1&crop=1&id=DPS5D&originHeight=335&originWidth=910&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- **定义结点**
```java
public class DoubleLinkedList<E> extends AbstractList<E> {
//定义首结点
private Node<E> first;
//定义为结点
private Node<E> last;
/**
* 定义链表中的结点
* @param <E>
*/
private static class Node<E> {
E element;
Node<E> prev; // 指向前驱节点
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 sb = new StringBuilder();
if(prev != null){
sb.append(prev.element);
}else{
sb.append("null");
}
sb.append("_").append(element).append("_");
if(next != null){
sb.append(next.element);
}else{
sb.append("null");
}
return sb.toString();
}
}
}
- 插入操作
- 插入位置小于size的1/2就从fisrt开始插入,大于1/2就从last开始插入
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
// size == 0
// index == 0
if (index == size) { // 往最后面添加元素
Node<E> oldLast = last;
last = new Node<>(oldLast, element, null);
if (oldLast == null) { // 这是链表添加的第一个元素
first = last;
} else {
oldLast.next = last;
}
} else { // 正常添加元素
Node<E> next = node(index);
Node<E> prev = next.prev;
Node<E> node = new Node<>(prev, element, next);
next.prev = node;
if (prev == null) { // index == 0
first = node;
} else {
prev.next = node;
}
}
size++;
}
- 删除操作
- 插入位置小于size的1/2就从fisrt开始遍历删除,大于1/2就从last开始遍历删除
@Override
public E remove(int index) {
rangeCheck(index);
Node<E> node = node(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;
}
双向链表接口定义
/**
* @author hguo
* @date2021/6/11 16:13
* @title 双向链表
*/
public class DoubleLinkedList<E> extends AbstractList<E> {
//定义首结点
private Node<E> first;
//定义为结点
private Node<E> last;
/**
* 定义链表中的结点
* @param <E>
*/
private static class Node<E> {
E element;
Node<E> prev; // 指向前驱节点
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 sb = new StringBuilder();
if(prev != null){
sb.append(prev.element);
}else{
sb.append("null");
}
sb.append("_").append(element).append("_");
if(next != null){
sb.append(next.element);
}else{
sb.append("null");
}
return sb.toString();
}
}
/**
* 清空元素
*/
@Override
public void clear() {
size = 0;
first = null;
last = null;
}
/**
* 获取元素
* @param index
* @return
*/
@Override
public E get(int index) {
return node(index).element;
}
/**
* 设置或修改元素
* @param index
* @param element
* @return
*/
@Override
public E set(int index, E element) {
Node<E> node = node(index);
E old = node.element;
node.element = element;
return old;
}
/**
* 插入元素
* @param index
* @param element
*/
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
if (index == size) { // 往最后面添加元素
Node<E> oldLast = last;
last = new Node<>(oldLast, element, null);
if (oldLast == null) { // 这是链表添加的第一个元素
first = last;
} else {
oldLast.next = last;
}
} else { // 正常添加元素
Node<E> next = node(index);
Node<E> prev = next.prev;
Node<E> node = new Node<>(prev, element, next);
next.prev = node;
if (prev == null) { // index == 0
first = node;
} else {
prev.next = node;
}
}
size++;
}
/**
* 删除元素
* @param index
* @return
*/
@Override
public E remove(int index) {
rangeCheck(index);
Node<E> node = node(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;
}
/**
* 索引元素
* @param element
* @return
*/
@Override
public int indexOf(E element) {
if (element == null) {
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (node.element == element) return i;
node = node.next;
}
} else {
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (node.element.equals(element)) return i;
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
/**
* 根据索引找到节点
*
* @param index
* @return
*/
private Node<E> node(int index) {
rangeCheck(index);
if (index < (size >> 1)) { // 索引小于一半从前往后找
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
} else { // 索引大于一半从后往前找
Node<E> node = last;
for (int i = size - 1; i > index; i--) {
node = node.prev;
}
return node;
}
}
/**
* 拼接并打印元素
* @return
*/
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(":[");
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (i != 0) {
string.append(", ");
}
string.append(node);
node = node.next;
}
string.append("]");
return string.toString();
}
}
3、测试
@Test
public void listTest1(){
ArrayList<Object> list1 = new ArrayList<>();
list1.add(12);
list1.add(13);
list1.add(14);
list1.add(15);
list1.add(16);
System.out.println(list1);
DoubleLinkedList<Object> list = new DoubleLinkedList<>();
list.add(12);
list.add(13);
list.add(14);
list.add(15);
list.add(16);
System.out.println(list);
}
3.3.7、单向链表、双向链表、动态数组
1、可操作性
- 相对于单向链表,双向链表可以减少将近一般的操作量,在效率上是有极大的提升
2、空间使用性
- 每对元素进行一次操作都会在原来的基础上开辟内存空间
好处是不会造成内存浪费
坏处是频繁开辟内存空间会导致效率降低
3.3.8、单向循环链表
单向循环链表,只有一个节点
/**
* 插入元素
*
* @param index
* @param element
*/
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
if (index == 0) {
Node<E> newFirst = new Node<>(element, first);
// 拿到最后一个节点, 上面先不要直接改first, 否则下面找节点会出现问题
Node<E> last = (size == 0) ? newFirst : node(size - 1);
last.next = newFirst;
first = newFirst;
} else {
Node<E> prev = node(index - 1);
prev.next = new Node<>(element, prev.next);
}
size++;
}
/**
* 删除元素
*
* @param index
* @return
*/
@Override
public E remove(int index) {
rangeCheck(index);
Node<E> node = first;
if (index == 0) {
if (size == 1) {
first = null;
} else {
Node<E> last = node(size - 1);
first = first.next;
last.next = first;
}
} else {
Node<E> prev = node(index - 1);
node = prev.next;
prev.next = node.next;
}
size--;
return node.element;
}
单向循环链表
/**
* @author hguo
* @date2021/6/13 15:23
* @title 单向循环链表
*/
public class SingleLoopLinkedList<E> extends AbstractList<E> {
//定义初始结点
private Node<E> first;
// 链表中的节点
private static class Node<E> {
E element; // 节点元素
Node<E> next; // 节点指向下一个节点
public Node(E element, Node<E> next) {
this.element = element;
this.next = next;
}
}
/**
* 根据索引找到节点对象
* @param index
* @return
*/
private Node<E> node(int index) {
rangeCheck(index);
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
@Override
public void clear() {
size = 0;
first = null;
}
@Override
public E get(int index) {
return node(index).element;
}
@Override
public E set(int index, E element) {
E old = node(index).element;
node(index).element = element;
return old;
}
/**
* 插入元素
* @param index
* @param element
*/
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
if (index == 0) {
Node<E> newFirst = new Node<>(element, first);
// 拿到最后一个节点, 上面先不要直接改first, 否则下面找节点会出现问题
Node<E> last = (size == 0) ? newFirst : node(size - 1);
last.next = newFirst;
first = newFirst;
} else {
Node<E> prev = node(index - 1);
prev.next = new Node<>(element, prev.next);
}
size++;
}
/**
* 删除元素
* @param index
* @return
*/
@Override
public E remove(int index) {
rangeCheck(index);
Node<E> node = first;
if (index == 0) {
if (size == 1) {
first = null;
} else {
Node<E> last = node(size - 1);
first = first.next;
last.next = first;
}
} else {
Node<E> prev = node(index - 1);
node = prev.next;
prev.next = node.next;
}
size--;
return node.element;
}
/**
* 查看元素的位置
* @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 {
for (int i = 0; i < size; i++) {
Node<E> node = first;
// 因为element肯定不为null了,所以放在前面;避免空指针异常
if (node.element.equals(element)) //比较数组即可,不用比较内存地址
{
return i;
}
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
//拼接字符串并打印
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(": [");
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (i != 0) {
string.append(", ");
}
string.append(node.element);
node = node.next;
}
string.append("]");
return string.toString();
}
}
测试
@Test
public void SingleLoopLinkedListTest(){
SingleLoopLinkedList<Integer> list = new SingleLoopLinkedList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(2,10);
list.add(5,11);
list.remove(1);
System.out.println(list);
}
}
3.3.9、双向循环链表
- 双向循环链表 - 只有一个节点
```java
//指针访问当前节点
private Node
current;
/**
- 删除 current 节点
*/
public E remove() {
}if (current == null) return null;
Node<E> next = current.next;
E element = remove(current);
if(size == 0){
current = null;
}else{
current = next;
}
return element;
@Override public void add(int index, E element) { rangeCheckForAdd(index);
if (index == size) { // 往最后面添加元素
Node<E> oldLast = last;
last = new Node<>(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 = node(index);
Node<E> prev = next.prev;
Node<E> node = new Node<>(prev, element, next);
next.prev = node;
prev.next = node;
if (next == first) { // index==0
first = node;
}
}
size++;
}
@Override
public E remove(int index) {
rangeCheck(index);
return remove(node(index));
}
public E remove(Node<E> node) {
if (size == 1) {
first = null;
last = null;
} else {
Node<E> prev = node.prev;
Node<E> next = node.next;
prev.next = next;
next.prev = prev;
if (node == first) { // index == 0
first = next;
}
if (node == last) { // index == size - 1
last = prev;
}
}
size--;
return node.element;
}
- **双向循环链表**
```java
/**
* @author hguo
* @date2021/6/13 15:55
* @title 双向循环链表
*/
public class DoubleLoopLinkedList<E> extends AbstractList<E> {
private Node<E> first;
private Node<E> last;
// 指针访问当前节点
private Node<E> current;
private static class Node<E> {
E element;
Node<E> prev;
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 sb = new StringBuilder();
if (prev != null) {
sb.append(prev.element);
} else {
sb.append("null");
}
sb.append("_").append(element).append("_");
if (next != null) {
sb.append(next.element);
} else {
sb.append("null");
}
return sb.toString();
}
}
public void reset() {
current = first;
}
public E next() {
if (current == null) return null;
current = current.next;
return current.element;
}
/**
* 删除 current 节点
*/
public E remove() {
if (current == null) return null;
Node<E> next = current.next;
E element = remove(current);
if(size == 0){
current = null;
}else{
current = next;
}
return element;
}
@Override
public void clear() {
size = 0;
first = null;
last = null;
}
@Override
public E get(int index) {
return node(index).element;
}
@Override
public E set(int index, E element) {
E old = node(index).element;
node(index).element = element;
return old;
}
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
if (index == size) { // 往最后面添加元素
Node<E> oldLast = last;
last = new Node<>(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 = node(index);
Node<E> prev = next.prev;
Node<E> node = new Node<>(prev, element, next);
next.prev = node;
prev.next = node;
if (next == first) { // index==0
first = node;
}
}
size++;
}
@Override
public E remove(int index) {
rangeCheck(index);
return remove(node(index));
}
public E remove(Node<E> node) {
if (size == 1) {
first = null;
last = null;
} else {
Node<E> prev = node.prev;
Node<E> next = node.next;
prev.next = next;
next.prev = prev;
if (node == first) { // index == 0
first = next;
}
if (node == last) { // index == size - 1
last = prev;
}
}
size--;
return node.element;
}
@Override
public int indexOf(E element) {
if (element == null) {
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (node.element == element)
return i;
node = node.next;
}
} else {
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (node.element.equals(element))
return i;
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
/**
* 根据索引找到节点
*/
private Node<E> node(int index) {
rangeCheck(index);
if (index < (size >> 1)) { // 索引小于一半从前往后找
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
} else { // 索引大于一半从后往前找
Node<E> node = last;
for (int i = size - 1; i > index; i--) {
node = node.prev;
}
return node;
}
}
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("[size=").append(size).append(", ");
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (i != 0) {
string.append(", ");
}
string.append(node);
node = node.next;
}
string.append("]");
return string.toString();
}
}
测试
@Test
public void Test2(){
DoubleLoopLinkedList<Object> list = new DoubleLoopLinkedList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(2,10);
list.add(7,12);
list.remove(3);
System.out.println(list);
}
链表优化
- current :用于指向某个节点
- void reset() :让 current 指向头结点 first
- E next():让 current 往后走一步,也就是 current = current.next
- E remove() :删除 current 指向的节点,删除成功后让 current 指向下一个节点
3.3.10、静态链表
- 以上所学的链表内容都依赖与指针
- 静态链表实质就是两个数组合并使用,一个数组存放索引关系,一个数组存放值
3.3.11、约瑟夫问题
1、约瑟夫环简介
Josephu约瑟夫环问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
2、约瑟夫环的实现
/**
* @author hguo
* @date2021/6/13 16:40
* @title 约瑟夫环问题
*/
public class JosephLoop {
static class Joseph{
public void josphloop(){
//创建双向循环链表对象
DoubleLoopLinkedList<Integer> list = new DoubleLoopLinkedList<>();
//循环移动元素所在位置
for (int i = 0; i < 8; i++) {
list.add(i);
}
//循环结点的一半
list.reset();
//结点不为零时往下移两次然后删除所在结点
while(!list.isEmpty()){
list.next();
list.next();
System.out.print(list.remove()+"\t");
}
}
}
public static void main(String[] args) {
Joseph joseph = new Joseph();
joseph.josphloop();
}
}
3.4、栈
3.4.1、什么是栈
- 栈是一种用于存储数据的简单数据结构,有点类似链表或者顺序表,栈与线性表的最大区别是数据的存取的操作
- 栈(Stack)是一种特殊的线性表,其插入和删除操作只允许在线性表的一端进行,把允许操作的一端称为栈顶(Top),不可操作的一端称为栈底(Bottom),同时把插入元素的操作称为入栈(Push),删除元素的操作称为出栈(Pop)。若栈中没有任何元素,则称为空栈
- 栈的特点
- 先进后出或后进先出
- 栈的操作
- 只能从栈顶存取元素,插入和删除元素都只能在栈顶操作,这是不同于顺序表和链表的
- 最先插入的元素最后操作,最后插入的元素最先操作
- 栈的基本操作创建栈,判空,入栈,出栈,获取栈顶元素
- 栈不支持指定位置的指定元素的指定操作
3.4.2、栈的运用场景
- 栈的运用大多都是做数据校验
- 符号匹配
- 中缀表达式转换为后缀表达式
- 计算后缀表达式
- 实现函数的嵌套调用
- HTML和XML文件中的标签匹配
- 软件任务撤销、恢复等功能
- 网页浏览器中已访问页面的历史记录
3.4.3、栈的使用
1、接口设计
- 接口规范内容
int size(); // 元素的数量
boolean isEmpty(); // 是否为空
void push(E element); // 入栈
E pop(); // 出栈
E top(); // 获取栈顶元素
void clear(); // 清空
组合形式实现父类接口的复用
/**
* @author hguo
* @date2021/6/14 12:52
* @title 栈的接口实现
*/
public class Stack<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(){
//调用remove()方法,删除栈顶元素
return list.remove(size()-1);
}
@Override
public String toString() {
return "list="+list;
}
}
继承ArrayList实现父类调用 ```java /**
- @author hguo
- @date2021/6/10 15:14
@title 动态数组接口设计 */ public class ArrayList
{ private int size; private E[] elements;
/**
- 数组的默认容量 */ private static final int DEFAULT_CAPACITY = 10;
/**
- 找不到元素返回-1 */ private static final int ELEMENT_NOT_FOUND = -1;
public ArrayList(int capacity) { // 设置默认容量为 10 capacity = Math.max(capacity, DEFAULT_CAPACITY);
// 因为泛型(所以传一个Object数组,然后通过强转) elements = (E[]) new Object[capacity]; }
public ArrayList() { // 默认容量 this(DEFAULT_CAPACITY); // 调用下面的构造器 }
//数组越界,异常处理 private void OutOfBounds(int index) { throw new IndexOutOfBoundsException(“Index:” + index + “, Size:” + size+”:数组越界,请正确输入!”); }
//限定CRUD操作范围 private void rangeCheck(int index) { if (index < 0 || index >= size) {
OutOfBounds(index);
} }
//插入操作的范围限定 private void rangeCheckForAdd(int index) { if (index < 0 || index > size) {
OutOfBounds(index);
} }
//数组扩容处理 private void ensureCapacity(int capcity){ //定义为扩容前的数组长度 int oldCapacity =elements.length; //判断容量大小 if(oldCapacity >= capcity){
return;
} //定义数组扩容的大小,1+0.5 int newCapacity = oldCapacity + (oldCapacity >> 1); E[] newElements = (E[]) new Object[newCapacity]; for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
} elements = newElements; System.out.println(oldCapacity+”扩容:”+newCapacity); }
/**
- 数组缩容
*/
public void trim() {
// 获取当前数组的容量
int oldcapacity = elements.length;
// 计算新的容量 = 原有容量的一半
int newCapacity = oldcapacity >> 1;
// 当size大于等于容量的一半, 或则容量已经小于默认容量(10)时, 直接返回
if (size >= newCapacity >> 1 || oldcapacity < DEFAULT_CAPACITY) return;
// 创建新数组
E[] newElements = (E[]) new Object[newCapacity];
// 将原数组元素存入新数组
for (int i = 0; i < size; i++) {
} // 引用新数组 elements = newElements; System.out.println(oldcapacity+”缩容:”+newCapacity); }newElements[i] = elements[i];
// 元素的数量 public int size() { return size; }
// 是否为空 public boolean isEmpty() { return size == 0; }
// 是否包含某个元素 public boolean contains(E element) { return indexOf(element)!= ELEMENT_NOT_FOUND; }
// 添加元素到最后面 public void add(E element) { add(size,element); }
// 返回index位置对应的元素 public E get(int index) { rangeCheck(index); return elements[index]; }
// 设置index位置的元素 public E set(int index, E element) { rangeCheck(index); E old = elements[index]; elements[index] = element; return old; }
// 往index位置添加元素 public void add(int index, E element) { //插入值为空时 if(element == null){
return;
} //处理数组越界 rangeCheckForAdd(index); //数组扩容 ensureCapacity(size+1); //从后往前开始操作 for (int i = size - 1; i >=index ; i—) {
elements[i + 1] =elements[i];
} elements[index] = element; size++; }
/**
- 删除index位置的元素
- @param index
@return 被删除的元素 */ public E remove(int index) { rangeCheck(index); E delEle = elements[index];
// 当删除一个元素时,需要挪动后面元素的范围 for (int i = index + 1; i <= size - 1; i++) {
elements[i - 1] = elements[i];
} size—; // 同clear的细节,当从后往前以后时,最后一个的地址需要释放 elements[size] = null; // 判断数组是否需要缩容 trim(); return delEle; }
/**
- 查看元素的位置
- @param element
- @return
*/
public int indexOf(E element) {
if(element == null){
}else {for (int i = 0; i < size; i++) {
if (elements[i] == null){
return i;
}
}
} return ELEMENT_NOT_FOUND; }for (int i = 0; i < size; i++) {
// 因为element肯定不为null了,所以放在前面;避免空指针异常
if (elements[i].equals(element)) //比较数组即可,不用比较内存地址
{
return i;
}
}
// 清除所有元素 public void clear() { for (int i = 0; i <size; i++) {
//清空数组
elements[i] = null;
} size = 0; //缩容 elements = (E[]) new Object[DEFAULT_CAPACITY]; }
@Override public String toString() { //使用StringBuilder进行字符串拼接 StringBuilder string = new StringBuilder(); //定义拼接格式 string.append(“size=”).append(size).append(“,[“); //循环遍历数组内容 for (int i = 0; i < size; i++) {
if(i != 0 ){
string.append(",");
}
string.append(elements[i]);
//元素用,隔开,但要判断元素是否为最后一个,最后一个不破解分隔符
} string.append(“]”); //打印数组内容 return string.toString(); } }
///继承ArrayList /**
- @author hguo
- @date2021/6/14 12:52
@title 栈的接口实现 */ public class Stack
extends ArrayList { //添加元素 public void push(E element){ //向栈顶添加元素
add(element);
}
//删除元素 public E pop(){
//调用remove()方法,删除栈顶元素
return remove(size()-1);
} } ```
测试
/**
* @author hguo
* @date2021/6/14 13:01
* @title 栈的测试
*/
public class StackTest {
@Test
public void Stack1(){
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
stack.push(5);
stack.push(6);
System.out.println("官方设计:");
System.out.println(stack);
while (!stack.isEmpty()){
System.out.println(stack.pop());
}
}
@Test
public void Stack2() {
com.hguo.stack.Stack<Object> stack = new com.hguo.stack.Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
stack.push(5);
stack.push(6);
System.out.println("黄氏自创:");
System.out.println(stack);
while (!stack.isEmpty()){
System.out.println(stack.pop());
}
}
}
复杂度分析
3.5、队列
3.5.1、什么是队列
- 队列是一种特殊的线性表,只能在头尾两端进行操作;
- 队尾(rear):只能从队尾添加元素,一般叫做
enQueue
,入队 - 队头(front):只能从队头移除元素,一般叫做
deQueue
,出队
- 队尾(rear):只能从队尾添加元素,一般叫做
- 队列的特征:先进先出或后进后出
- 队列的常规操作
- 初始化队列
- 判断队列是否为空或满
- 入队: 通常命名为push()
- 出队: 通常命名为pop()
- 获取队首元素
- 显示队列元素
- 进行入队操作时,要先判断队列是否为满
- 进行出队操作时,要先判断队列是否为空
- 缺点
- 操作是如果出队列比较多,要搬移大量元素
- 如果还有新元素进行入队列容易造成假溢出
- 假溢出:顺序队列因多次入队列和出队列操作后出现的尚有存储空间但不能进行入队列操作的溢出。
- 真溢出:顺序队列的最大存储空间已经存满二又要求进行入队列操作所引起的溢出。
- 队列的存储结构
- 队列与栈一样是一种线性结构,因此以常见的线性表如数组、链表作为底层的数据结构。
- 操作拓展(C语言)
- 以下操作均在在链队列中进行
- 入队操作,创建一个新结点p,其data域值为x,指针域为空,入队操作步骤:
- 插入元素:队尾插入元素,指针域Q->rear->next指向新结点p,(Q->rear->next=p)
- 队尾Q->rear指向新结点p,(Q->rear=p)
- 出队操作,队头指针p指向队头元素的结点,并将队头元素取出赋给指针x所指的变量
- 队头指针指向下一个元素,Q->front->next=p->next
- 出队后队尾指针指向对头指针,Q->rear=Q->front
3.5.2、队列接口设计
- 队列的插入操作一般是在队尾进行,删除操作一般是在对头进行
- 频繁的头尾操作使得效率会受一定程度的影响,因此队列的接口设计使用双向链表作为支持比较符合规范
单向队列接口设计
/**
* @author hguo
* @date2021/6/14 13:45
* @title 队列的接口实现
*/
public class Queue<E> {
//使用组合设计模式
private List<E> list = new DoubleLinkedList<>();
// 元素的数量
int size()
{
return list.size();
}
// 查看队列是否为空
boolean isEmpty()
{
return list.isEmpty();
}
// 清空元素
void clear()
{
list.clear();
}
// 入队,添加元素
void enQueue(E element)
{
list.add(element);
}
// 出队,删除元素
E deQueue()
{
return list.remove(0);
}
// 获取队列的头元素
E front()
{
return list.get(0);
}
@Override
public String toString() {
return
"list=" + list;
}
}
- 测试
/**
* @author hguo
* @date2021/6/14 13:56
* @title 单向队列测试
*/
public class QueueTest {
public static void main(String[] args) {
Queue<Integer> queue = new Queue<>();
queue.enQueue(11);
queue.enQueue(12);
queue.enQueue(13);
queue.enQueue(14);
queue.enQueue(15);
System.out.println(queue);
while (!queue.isEmpty()){
System.out.println(queue.deQueue());
}
}
}
3.5.3、双端队列
1、双端队列优势
- 双端队列:double ended queue == deque
- 双端队列在首尾两个位置都可以对元素进行删除和插入操作
- 相对于单向队列,双端队列提高了操作的效率,在一定程度上可以防止假溢出
2、双端队列的实现
- 双端队列的接口设计
int size(); // 元素的数量
boolean isEmpty(); // 是否为空
void clear(); // 清空
void enQueueRear(E element); // 从队尾入队
E deQueueFront(); // 从队头出队
void enQueueFront(E element); // 从队头入队
E deQueueRear(); // 从队尾出队
E front(); // 获取队列的头元素
E rear(); // 获取队列的尾元素
完整父类继承接口
/**
* @author hguo
* @date2021/6/14 14:25
* @title 双端队列的接口实现
*/
public class Deque<E> {
// 使用组合模式,双向链表实现双端队列
private List<E> list = new DoubleLinkedList<>();
/**
* 元素的数量E
*/
public int size(){
return list.size();
}
/**
* 是否为空
*/
public boolean isEmpty(){
return list.isEmpty();
}
/**
* 清空
*/
public void clear(){
list.clear();
}
/**
* 从队尾入队
*/
public void enQueueRear(E element){
list.add(element);
}
/**
* 从队头入队
*/
public void enQueueFront(E element){
list.add(0, element);
}
/**
* 从队尾出队
*/
public E deQueueRear(){
return list.remove(list.size() - 1);
}
/**
* 从队头出队
*/
public E deQueueFront(){
return list.remove(0);
}
/**
* 获取队列的头元素
*/
public E front(){
return list.get(0);
}
/**
* 获取队里的尾元素
*/
public E rear(){
return list.get(list.size() - 1);
}
@Override
public String toString() {
return
"list=" + list ;
}
}
- 测试
@Test
public void dequeTest(){
Deque<Object> deque = new Deque<>();
deque.enQueueFront(1);
deque.enQueueFront(2);
deque.enQueueRear(3);
deque.enQueueRear(4);
System.out.println(deque);
while (!deque.isEmpty()){
System.out.print(deque.deQueueFront()+"\t");
System.out.print(deque.deQueueRear() + "\t");
}
}
3.5.4、循环队列
- 循环队列:底层实现依然是数组,在动态控制容量的情况下,使用动态数组作为循环队列的支持是复杂度最小的实现方法
- 循环队列的实现原理
- 元素的循环操作位置:index=(front+size)%num
- front=2
- size=5
- num=7
- index=(front+size)%num=0
- 元素的循环操作位置:index=(front+size)%num
循环列队的实现
/**
* @author hguo
* @date2021/6/14 15:12
* @title 循环队列
*/
@SuppressWarnings("unchecked")
public class LoopQueue<E> {
private int front; // 队头指针
private int size; // 元素数量
// 利用动态扩容数组实现的循环队列
private E elements[]; // 元素
public static final int DEFAULT_CAPACITY = 10; // 初始容量
public LoopQueue() {
elements = (E[]) new Object[DEFAULT_CAPACITY];
}
/**
* 元素的数量
*/
public int size() {
return size;
}
/**
* 是否为空
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 清空
*/
public void clear() {
for (int i = 0; i < size; i++) {
// elements[index(i)] = null;
elements[(i + front) %elements.length] = null;
}
size = 0;
front = 0;
}
/**
* 从队头出队
*/
public E deQueue() {
E fronElement = elements[front];
elements[front] = null;
front = (front + 1) % elements.length;
size--;
return fronElement;
}
/**
* 从队尾入队
*/
public void enQueue(E element) {
// 扩容
ensureCapacity(size + 1);
elements[(front + size) % elements.length] = element;
size++;
}
/**
* 获取队列的头元素
*/
public E front() {
return elements[front];
}
// 扩容
private void ensureCapacity(int capacity) {
int oldCapacity = elements.length;
if (oldCapacity >= capacity)
return;
// 新容量为旧容量的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElements = (E[]) new Object[newCapacity];
// 旧数组中元素移到新数组
for (int i = 0; i < size; i++) {
//获取真实索引
newElements[i] = elements[(i + front) % elements.length];
}
System.out.println("从" + oldCapacity + "扩容到" + newCapacity);
elements = newElements;
front = 0; // 重置front
}
@Override
public String toString() {
return "LoopQueue{" +
"front=" + front +
", size=" + size +
", elements=" + Arrays.toString(elements) +
'}';
}
}
- 测试
@Test
public void loopqueueTest(){
LoopQueue<Integer> queue = new LoopQueue<>();
for (int i = 0; i < 10; i++) {
queue.enQueue(i);
}
for (int i = 0; i < 5; i++) {
queue.deQueue();
}
for (int i = 15; i < 25; i++) {
queue.enQueue(i);
}
System.out.println(queue);
while (!queue.isEmpty()){
System.out.print(queue.deQueue()+"\t");
}
}
3.5.5、循环双端队列
- 循环双端队列:可以进行两端添加、删除操作的循环队列;
- 循环队列中用了 front 指针来表示队列的头部,而在循环中头指针便可以算出尾部
- 首先理解一下循环双端队列中索引封装映射
- 传入的 index 是相对于 front 的索引,返回的是真实的索引:
- 获得头部指针的前一位,则是 index(-1)(用于队头入队)
- 获得尾部指针,则是 index(size - 1)
双端循环队列实现
/**
* @author hguo
* @date2021/6/14 16:42
* @title 双端循环队列的实现
*/
@SuppressWarnings("all")
public class LoopDeque<E> {
private int front; // 队头指针
private int size; // 元素数量
private E elements[]; // 元素
public static final int DEFAULT_CAPACITY = 10; // 初始容量
public LoopDeque() {
elements = (E[]) new Object[DEFAULT_CAPACITY];
}
/**
* 元素的数量
*/
public int size() {
return size;
}
/**
* 是否为空
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 清空
*/
public void clear() {
for (int i = 0; i < size; i++) {
elements[index(i)] = null;
}
front = 0;
size = 0;
}
/**
* 从队尾入队
*/
public void enQueueRear(E element) {
ensureCapacity(size + 1);
elements[index(size)] = element;
size++;
}
/**
* 从队头入队
*/
public void enQueueFront(E element) {
ensureCapacity(size + 1);
//队首的索引为0,0的前面插入数据
front = index(-1);
elements[front] = element;
size++;
}
/**
* 从队尾出队
*/
public E deQueueRear() {
int rearIndex = index(size - 1);
E rear = elements[rearIndex];
elements[rearIndex] = null;
size--;
return rear;
}
/**
* 从队头出队
*/
// 头 1 r(2) null null f(5) 6 7 8 9 尾
public E deQueueFront() {
E frontElement = elements[front];
elements[front] = null;
front = index(1);
size--;
return frontElement;
}
/**
* 获取队列的头元素
*/
public E front() {
return elements[front];
}
/**
* 获取队列的尾元素
*/
public E rear() {
return elements[index(size - 1)];
}
// 索引封装映射
private int index(int index) {
index += front;
if (index < 0) { // index 为负数
return index + elements.length;
}
// index 为正数
return index % elements.length;
}
// 数组扩容
private void ensureCapacity(int capacity) {
int oldCapacity = elements.length;
if (oldCapacity >= capacity)
return;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容为1.5倍
E newElements[] = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[index(i)];
}
System.out.println("从" + oldCapacity + "扩容到" + newCapacity);
elements = newElements;
front = 0; // 重置front
}
@Override
public String toString() {
return "LoopDeque{" +
"front=" + front +
", size=" + size +
", elements=" + Arrays.toString(elements) +
'}';
}
}
测试
@Test
public void loopdequeTest(){
LoopDeque<Integer> queue = new LoopDeque<>();
for (int i = 0; i < 10; i++) {
queue.enQueueFront(i+1);
queue.enQueueRear(i+100);
}
for (int i = 0; i < 3; i++) {
queue.deQueueFront();
queue.deQueueRear();
}
queue.enQueueFront(11);
queue.enQueueRear(12);
System.out.println(queue);
while (!queue.isEmpty()){
System.out.print(queue.deQueueFront()+"\t");
}
}
分析
头尾循环10次插入元素
- 第一次扩容前,6是队首104是队尾,第一次扩容长度为5,8是队首107是队尾,第二次扩容长度为7,10是队首109是队尾
LoopDeque<Integer> queue = new LoopDeque<>();
for (int i = 0; i < 10; i++) {
queue.enQueueFront(i+1);
queue.enQueueRear(i+100);
}
System.out.println(queue);
while (!queue.isEmpty()){
System.out.print(queue.deQueueFront()+"\t");
}
- 第一次扩容前,6是队首104是队尾,第一次扩容长度为5,8是队首107是队尾,第二次扩容长度为7,10是队首109是队尾
头尾循环删除元素
- 队首队尾同时删除三个元素,队首 -10、-9、-8,队尾-109、-108、-107
LoopDeque<Integer> queue = new LoopDeque<>();
for (int i = 0; i < 10; i++) {
queue.enQueueFront(i+1);
queue.enQueueRear(i+100);
}
for (int i = 0; i < 3; i++) {
queue.deQueueFront();
queue.deQueueRear();
}
System.out.println(queue);
while (!queue.isEmpty()){
System.out.print(queue.deQueueFront()+"\t");
}
- 队首队尾同时删除三个元素,队首 -10、-9、-8,队尾-109、-108、-107
对头插入11,队尾插入12
@Test
public void loopdequeTest(){
LoopDeque<Integer> queue = new LoopDeque<>();
for (int i = 0; i < 10; i++) {
queue.enQueueFront(i+1);
queue.enQueueRear(i+100);
}
for (int i = 0; i < 3; i++) {
queue.deQueueFront();
queue.deQueueRear();
}
queue.enQueueFront(11);
queue.enQueueRear(12);
System.out.println(queue);
while (!queue.isEmpty()){
System.out.print(queue.deQueueFront()+"\t");
}
}
3.5.6、优先队列
四、非线性数据结构一
4.1、多维数组
4.2、串和广义表
五、非线性数据结构二
5.1、树
5.1.1、树的基本概念
1、树的分类
- 树按照性质分为:
- 多叉树
- 二叉树
- 二叉树下分为
- 有序树、无序树、森林
2、树的基本概念
- 以下图为例进行分析
- 节点:树上的每一个元素都称为节点,一棵树可以没有节点称为空树,也可以只有一个节点(1、2、3……223)
- 根节点:层级做高的结点称为根节点,一棵树只有一个根节点(1)
- 父节点:节点下面有其他结点,该节点称为父节点(1、2、3、5、6)
- 兄弟节点:同一父节点下的所有子节点称为兄弟节点(21、22)
- 子节点:上层节点生成的节点称为子节点,如(2、3、4、5、6…….)
- 子树:节点层级大于2的都可以称为子树(2、21、22、221、222、223)
- 左子树:相对根数位置在左的子树(2、21、22、221、222、223)
- 右子树:相对根数位置在右的子树(6、61)
- 节点的度(degree):子树的个数(1的度为5 <2、3、4、5、6>)
- 树的度:所有节点度中的最大值(该树的度为5 <2、3、4、5、6>)
- 叶子节点(leaf):度为 0 的节点(21、221、222、223、31、4、51、52、61)
- 非叶子节点:度不为 0 的节点(2、22……6)
- 层数(level):根节点在第 1 层,根节点的子节点在第 2 层,以此类推 (该树有4层)
- 节点的深度(depth):从根节点到当前节点的唯一路径上的节点总数(以222为例,路径:1、2、22、222,深度=4)
- 节点的高度(height):从当前节点到最远叶子节点路径上的节点总数(以1为例,最远路径:1、2、22、221/222/223,高度=4)
- 树的深度:所有节点深度中的最大值
- 树的高度:所有节点高度中的最大值
- 数的深度 等于 树的高度,树由多高就有多深
- 有序树:树的每层元素节点都严格遵守从左到右依次增大,有序就是从左到,右从小到大
- 无序树:树的每层元素节点的排列没有顺序
- 森林:由m棵互不相交的数组成的集合,就是很多树
5.1.2、二叉树的性质与特点
1、二叉树的特点:
- 每个节点的度最大为 2(最多拥有 2 棵子树)
- 左子树和右子树是有顺序的,二叉树是有序树
- 即使某节点只有一棵子树,也要区分左右子树
- 每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
- 左子树和右子树是有顺序的,次序不能任意颠倒。
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
- 以下都是二叉树
2、二叉树的性质:
- 非空二叉树的第 i 层,最多有 2^(i−1) 个节点( i ≥ 1 )
- 在高度为 h 的二叉树上最多有 2^(h)-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
- (1) 若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点;
- (2) 若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点;
- (3) 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点
3、真二叉树
- 真二叉树:所有节点的度都要么为 0,要么为 2
- 不是二叉树
4、满二叉树
- 满二叉树:在一棵二叉树中。如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
- 满二叉树特点:
- 1)叶子只能出现在最下一层。出现在其它层就不可能达成平衡。
- 2)非叶子结点的度一定是2。
- 3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
- 满二叉树:最后一层节点的度都为 0,其他节点的度都为 2
- 假设满二叉树的高度为 h( h ≥ 1 ),那么
- 第 i 层的节点数量: 2^(i−1)
- 叶子节点数量: 2^(h)−1
- 总节点数量 n
- n = 2h − 1 = 20 + 21 + 22 + ⋯ + 2h−1
- 树高度与总节点的关系:h = log2(n + 1)
- 在同样高度的二叉树中,满二叉树的叶子节点数量最多、总节点数量最多;
- 满二叉树一定是真二叉树,真二叉树不一定是满二叉树;
- 该树第四层节点:2^(4-1)=8
- 该树叶子总数:2^(4)-1=15
5、完全二叉树
- 完全二叉树:叶子节点只会出现在最后两层,且最后一层叶子节点都靠左对齐
- 对节点从上至下、左至右开始编号,其所有编号都能与相同高度的满二叉树中的编号对应
- 满二叉树是完全二叉树,但完全二叉树不一定是满二叉树
- 完全二叉树特点:
- 1)叶子结点只能出现在最下层和次下层。
- 2)最下层的叶子结点集中在树的左部。
- 3)倒数第二层若存在叶子结点,一定在右部连续位置。
- 4)如果结点度为1,则该结点只有左孩子,即没有右子树。
- 5)同样结点数目的二叉树,完全二叉树深度最小。
- 注:满二叉树一定是完全二叉树,但反过来不一定成立。
- 完全二叉树的性质
- 度为 1 的节点只有左子树
- 度为 1 的节点要么是 1 个,要么是 0 个
- 同样节点数量的二叉树,完全二叉树的高度最小
- 假设完全二叉树的高度为 h( h ≥ 1 ),那么:
- 至少有 2h−1 个节点 ( 20 + 21 + 22 + ⋯ + 2h−2 + 1 )
- 最多有 2h − 1 个节点( 20 + 21 + 22 + ⋯ + 2h−1,即 满二叉树 )
- 总节点数量为 n
- 2h−1 ≤ n < 2h
- h − 1 ≤ log2n < h
- h = floor( log2n ) + 1
- ( floor 是向下取整,ceiling 是向上取整 )
- 完全二叉树计算
- 小试牛刀
5.1.3、二叉树的存储结构
1、顺序存储
- 二叉树的顺序存储结构就是使用一维数组存储二叉树中的结点,并且结点的存储位置,就是数组的下标索引。
- 一棵完全二叉树采用顺序存储方式得到的结果为
- 当二叉树不为完全二叉树时的顺序存储
- 其中浅色结点表示结点不存在,使用/\填充
- 对于这种右斜树极端情况,采用顺序存储的方式是十分浪费空间的。因此,顺序存储一般适用于完全二叉树。
2、二叉链表
- 顺序存储不能满足二叉树的存储需求,二叉树的每个结点最多有两个孩子。因此,可以将结点数据结构定义为一个数据和两个指针域。
- 用二叉树表示,采用一种链表结构存储二叉树,这种链表称为二叉链表
5.1.4、遍历二叉树
- 遍历是数据结构中的常见操作:把所有元素都访问一遍;
- 线性数据结构的遍历;
- 正序遍历
- 逆序遍历
1、前序遍历
- 访问顺序:根节点、左子树、右子树
- 前序遍历结果:7、4、2、1、3、5、9、8、11、10、12
2、中序遍历
- 访问顺序::左子树、根节点、右子树
- 中序遍历结果:1、2、3、4、5、7、8、9、10、11、12
3、后续遍历
- 访问顺序:左子树、右子树、根节点
- 后续遍历结果:1、3、2、5、4、8、10、12、11、9、7
4、层序遍历
- 从上到下、从左到右依次访问每一个节点
- 层序遍历结果:7、4、9、2、5、8、11、1、3、10、12
5、重构二叉树
- 前序遍历 + 中序遍历确定唯一二叉树
- 后序遍历 + 中序遍历确定唯一二叉树
- 前序遍历 + 后序遍历不能确定唯一二叉树
- 前驱节点:中序遍历根节点的前一个节点元素
- 后驱节点:中序遍历根节点的最后一个节点元素
5.1.5、二叉搜索树
- 二叉排序树 == 二叉查找树 == 二叉搜索树
1、二叉搜索树的内涵
- 二叉搜索树是二叉树的一种,是应用非常广泛的一种二叉树
- 二叉搜索树也称二叉查找树
- 二叉搜索的特点
- 任意一个节点的值都大于其左子树所有节点的值
- 任意一个节点的值都小于其右子树所有节点的值
- 二分法的查找过程中,n个有序元素构成的序列,查找的时间复杂度为 O(log2n)
- 二叉搜索树的查询复杂度只是介于 O(h)=O(log2n)~O(n) 之间
- 二叉搜索树存储的元素必须具备可比较性
- 左右子树都是二叉搜索树
- 二叉搜索树不能为null
2、四则运算与表达式
- 四则运算的表达式可以分为3种:
- 前缀表达式(prefix expression),又称为波兰表达式
- 中缀表达式(infix expression)
- 后缀表达式(postfix expression),又称为逆波兰表达式
- 树的表达
- 反转二叉树
- 将输入的元素以根节点和父节点为中心,所有子节点左右翻转
public class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null)
return root;
TreeNode node = root.left;
root.left = root.right;
root.right = node;
invertTree(root.left);
invertTree(root.right);
return root;
}
}
- 将输入的元素以根节点和父节点为中心,所有子节点左右翻转
5.1.6、平衡二叉树
1、二叉搜索树的缺点分析
- 当 n 比较大时,两者的性能差异比较大
- 比如 n = 1000000 时,二叉搜索树的最低高度是 (logn) 20,最高高度是(n) 1000000;
- 二叉搜索树添加和删除节点时可能会导致二叉搜索树退化成链表,造成时间复杂度成倍增加;
2、平衡二叉树
- 当节点数量固定时,左右子树的高度越接近,这棵二叉树就越平衡(高度越低)
- 理想平衡二叉树
- 最理想的平衡,就是像完全二叉树、满二叉树那样,高度是最小的;
- 改进二叉搜索树
- 在节点的添加、删除操作之后,想办法让二叉搜索树恢复平衡(减小树的高度)
- 如果接着继续调整节点的位置,完全可以达到理想平衡,但是付出的代价可能会比较大;
- 比如调整的次数会比较多,反而增加了时间复杂度;
- 总结来说,比较合理的改进方案是:用尽量少的调整次数达到适度平衡即可;
一棵达到适度平衡的二叉搜索树,可以称之为:平衡二叉搜索树
3、平衡二叉搜索树
- BBST == Balanced Binary Search Tree
- 常见的平衡二叉树:自平衡的二叉搜索树(Self-balancing Binary Search Tree)
- AVL树
Windows NT 内核中广泛使用 - 红黑树
C++ STL(比如 map、set )
Java 的 TreeMap、TreeSet、HashMap、HashSet
Linux 的进程调度
Ngix 的 timer 管理
- AVL树
5.1.7、AVL树
1、AVL树
- AVL 树是最早发明的自平衡二叉搜索树之—
- 平衡因子(Balance Factor):某结点的左右子树的高度差
- 平衡因子 = 左子树高度 - 右子树高度
- 叶子节点的平衡因子=0
- AVL树的特点:
- 每个节点的平衡因子只可能是 1、0、-1
- (绝对值 ≤ 1,如果超过 1,称之为 “失衡”)
- 每个节点的左右子树高度差不超过1
- 搜索、添加、删除的时间复杂度是O(logn)
- AVL树自平衡的关键就在于节点的左右子树高度差不超过1
2、二叉搜索树与AVL树
- 输入数据:35, 37, 34, 56, 25, 62, 57, 9, 74, 32, 94, 80, 75, 100, 16, 82
- AVL树的失衡
- 添加节点导致失衡
- 往该平衡树中添加元素13导致平衡树失衡
- 添加节点导致失衡
- 最坏情况:可能会导致所有祖先节点都失衡
- 父节点、非祖先节点,都不可能失衡
- 删除节点导致失衡
- 可能会导致父节点或祖先节点失衡(只有1个节点会失衡),其他节点,都不可能失衡
- 删除该平衡树中的元素16导致失衡
3、AVL树的旋转
- 旋转实质是平衡修复的操作
- LL – 右旋转(单旋)
- RR – 左旋转(单旋)
- LR – 先左旋,再右旋(双旋)
- RL – 先右旋,再左旋(双旋)
- AVL树总结
- 添加
- 可能会导致所有祖先节点都失衡
- 只要让高度最低的失衡节点恢复平衡,整棵树就恢复平衡【仅需 O(1) 次调整】
- 删除
- 可能会导致父节点或祖先节点失衡(只有1个节点会失衡)
- 恢复平衡后,可能会导致更高层的祖先节点失衡【最多需要 O(logn) 次调整】
- 平均时间复杂度
- 搜索:O(logn)
- 添加:O(logn),仅需 O(1) 次的旋转操作
- 删除:O(logn),最多需要 O(logn) 次的旋转操作
- 添加
5.1.8、B树、红黑树
1、B树
- B树是一种平衡的多路搜索树,多用于文件系统、数据库的实现;
- B树的特点
- 1 个节点可以存储超过 2 个元素、可以拥有超过 2 个子节点
- 拥有二叉搜索树的一些性质
- 平衡,每个节点的所有子树高度一致
- 比较矮
- m阶B树的性质
- m阶的含义:树中最多有m个子节点
- 数据库使用的B树一般为200~300阶
- B树与二叉搜索树
- B树 和 二叉搜索树,在逻辑上是等价的;多代节点合并,可以获得一个超级节点
- 2 代合并的超级节点,最多拥有 4 个子节点(至少是 4 阶B树)
- 3 代合并的超级节点,最多拥有 8 个子节点(至少是 8 阶B树)
- n 代合并的超级节点,最多拥有 2n 个子节点( 至少是 2n 阶B树)
- m 阶 B树,最多需要 log2m 代合并;
- B树 和 二叉搜索树,在逻辑上是等价的;多代节点合并,可以获得一个超级节点
- B树的操作
- 搜索
- 添加
- 新添加的元素必定是添加到叶子节点:
- m阶树右子树叶子节点的元素个数上线即为m,超过限制就会发生上溢现象
- 解决元素上溢问题
- 将 k 位置的元素向上与父节点合并
- 将 [0, k - 1] 和 [k + 1, m - 1] 位置的元素分裂成 2 个子节点
- 这 2 个子节点的元素个数,必然都不会低于最低限制(┌ m/2 ┐ − 1)
- 防止父节点也上溢,将新添加的元素提出作为新的父节点
- 元素添加结果
- 删除
- 假如需要删除的元素在叶子节点中,那么直接删除即可;
- 一个元素的父节点只能由两个子节点
- 删除 – 非叶子节点
- 假如需要删除的元素在非叶子节点中
- 先找到前驱或后继元素,覆盖所需删除元素的值
- 非叶子节点的前驱或后继元素,必定在叶子节点中
- 所以删除的是前驱或后继元素 ,就是最开始提到的情况:删除的元素在叶子节点中
- 真正的删除元素都是发生在叶子节点中;
- 再把前驱或后继元素删除
- 前驱:左子树的右子树
- 后驱:右子树的左子树
- 假如需要删除的元素在非叶子节点中
- 删除-下溢
- 下溢节点的元素数量必然等于 ┌ m/2 ┐ − 2
- 如果下溢节点临近的兄弟节点,有至少 ┌ m/2 ┐ 个元素,可以向其借一个元素
- 将父节点的元素 b 插入到下溢节点的 0 位置(最小位置)
- 用兄弟节点的元素 a(最大的元素)替代父节点的元素 b
- 这种操作其实就是:旋转
- 如果下溢节点临近的兄弟节点,**只有 ┌ m/2 ┐ − 1 个元素**
- 将父节点的元素 b 挪下来跟左右子节点**进行合并**
- **合并后的节点元素个数等于┌ m/2 ┐ + ┌ m/2 ┐ − 2,不超过 m − 1**
- 这个操作可能会导致父节点下溢,依然按照上述方法解决,下溢现象可能会一直往上传播
2、B+树
- B+ 树是一种树数据结构,是一个n叉树,每个节点通常有多个孩子,一颗B+树包含根节点、内部节点和叶子节点
- B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度
- 常用于数据库和操作系统的文件系统中
- B+ 树元素自底向上插入
- 一个m阶的B树具有如下几个特征:
1.根结点至少有两个子女。
2.每个中间节点都至少包含ceil(m / 2)
个孩子,最多有m个孩子。
3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m。
4.所有的叶子结点都位于同一层。
5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。 - 每个叶子节点都有一个指针,指向下一个数据,形成一个有序链表。
- B+树与B树的区别
- 有k个子结点的结点必然有k个关键码;
- 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
- 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。
- 添加过程
- 超出节点定义的范围,中间节点上移成为父节点,分裂成两段增加子节点区间范围,中间节点属于右子树
- 依次插入8,10,15
- 插入16,节点溢出,分裂成两段
- 插入17、18再溢出,再分裂
- 插入若干后,父节点也溢出,父节点同样的进行分裂
- 删除过程
- 若删除的元素是叶子节点的非零节点(叶子节点>2),则可以直接删除
- 若删除的是叶子节点的零节点元素(叶子节点<=2),则需要该叶子节点的前驱节点覆盖该节点,并删除该前驱节点
d) 删除7后父节点小于5,节点合并
3、红黑树
- 红黑树的性质:
- 根节点和叶子节点必须是黑节点
- 红节点的父节点和子节点必须是黑节点
- 不能出现连续的两个红节点
- 任意一个节点到叶子节点路径上的的黑节点数量相同
- 红黑树自平衡的关键再于左右子节点的黑节点数量一致且左右子节点的高度差不超过2倍
- 红黑树自平衡的过程
- 输如元素逐渐增大的情况,选择左旋转进行自平衡,父节点和叔叔节点同色且为红则换色在操作,父节点和叔叔节点异色或黑子就进行左旋转再变色
- 输如元素逐渐减小的情况,选择右旋转进行自平衡,父节点和叔叔节点同色且为红则换色在操作,父节点和叔叔节点异色或黑子就进行右旋转再变色
- 输入随机大小的情况,就得按照要求进行左旋或右旋自平衡后变色
- 红黑树插入总结
红黑树过于复杂,不是你我能研究透彻的,入门到入土就在一瞬间
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210617144344105.png#crop=0&crop=0&crop=1&crop=1&id=OeopR&originHeight=292&originWidth=817&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
4、Huffman树
5.1.9、List、Set、ArrayList、HashSet、TreeSet
- Collection
- List:元素是有序且重复。因为该集合体系有索引。
- ArrayList:底层使用的是有序数组结构。
- 特点:查询速度很快O(1)。但是增删稍慢O(n)。线程不安全。
- LinkedList:底层使用的链表数据结构。
- 特点:增删速度很快O(1),查询稍慢O(n)。线程不安全。
- Vector:底层是数组数据结构,如果不是多个线程操作集合用ArrayList。因为Vector。
- 特点:效率低但线程同步。
- Set:元素是无序(存取顺序不一定一致),元素不可以重复。
- HashSet:底层数据结构是哈希表。
- 保证元素唯一性是判断元素的HashCode是否相同的依据,相同将继续判断元素的equals方法是否为true,线程不安全。
- TreeSet: 底层数据结构是二叉排序树。根据compareTo方法的返回值来判断元素大小。可以对Set集合中的元素进行排序,可指定按某种规则排序。
- 可以用于个map集合中的键进行排序,线程不安全
- HashMap:底层是哈希表数据结构,可以存入null键和null值。
- 效率较高,线程不安全
- HashTable:底层是哈希表数据结构,不可以存入null键和null值。
- 效率比较低,线程安全
- 集合不存储重复元素,数组,链表等均可以存储重复元素,集合多用于去重
- 总结:
- 线程安全且低效
- Vector
- HashTable
- 线程不安全且高效
- ArrayList
- LinkedList
- HashSet
- TreeMap
- 线程安全且低效
5.1.10、哈希及哈希表
1、哈希
- 一种储存结构,通过某种函数,使得其元素的储存位置与他的关键码之间能够建立一一映射关系,那么在查找时通过该函数很快找到相应元素
- 哈希表、哈希编码、哈希函数在开发中都是极其重要的
- 哈希函数
- 先生成 key 的哈希值(必须是整数)
- 再让 key 的哈希值 跟 数组的大小 进行相关运算,生成一个 索引值
- 为了提高效率,可以使用
**&**
位运算 取代**%**
运算
2、哈希表及哈希冲突
- 哈希表也叫做散列表(hash有“剁碎”的意思)
- 利用哈希函数生成 key 对应的 index【O(1)】
- 根据 index 操作定位数组元素【O(1)】
- 哈希表添加、搜索、删除的流程都是类似的
- 哈希表是【空间换时间】的典型应用
- 哈希函数,也叫做 散列函数
- 哈希表内部的数组元素,很多地方也叫 Bucket(桶),整个数组叫 Buckets 或者 Bucket Array
- 哈希表的存储
- 哈希冲突
- 哈希冲突 也叫做 哈希碰撞
- 2个不同的 key,经过哈希函数计算出相同的结果
- key1 ≠ \neq\= key2, hash(key1) = hash(key2),索引相同但存储的值不同,就会产生冲突
- 解决哈希冲突的方法
- 开放定址法(Open Addressing)
按照一定规则向其他地址探测,直到遇到空桶 - 再哈希法(Re-Hashing)
设计多个哈希函数 - 链地址法(Separate Chaining)
比如通过链表将同一 index 的元素串起来 - 默认使用单向链表将元素串起来
- 在添加元素时,可能会由单向链表转为红黑树来存储元素
比如当哈希表容量 ≥ 64 且 单向链表的节点数量大于 8 时 - 当红黑树节点数量少到一定程度时,又会转为单向链表
- JDK1.8 中的哈希表是使用 链表+红黑树 解决哈希冲突
- 开放定址法(Open Addressing)
- 使用单向链表而不使用双向链表的原因
- 每次都是从头节点开始遍历
- 单向链表比双向链表少一个指针,可以节省内存空间
5.1.11、堆
1、堆的定义
- 堆就是用数组实现的二叉树,所以它没有使用父指针或者子指针。堆根据“堆属性”来排序,“堆属性”决定了树中节点的位置。
- 堆的常用方法:
- 构建优先队列
- 支持堆排序
- 快速找出一个集合中的最小值(或者最大值)
- 在朋友面前装逼
- 堆的性质
- 堆分为最大堆和最小堆,两者的差别在于节点的排序方式上
- 在最大堆中,父节点的值比每一个子节点的值都要大。
- 在最小堆中,父节点的值比每一个子节点的值都要小。
- 它是完全二叉树,除了树的最后一层结点不需要是满的,其它的每一层从左到右都是满的,如果最后一层结点不是满的,那么要求左满右不满。
- 堆实质还是通过数组来实现的
- 是将二叉树的结点按照层级顺序放入数组中,根结点在位置1,它的子结点在位置2和3,而子结点的子 结点则分别在位置4,5,6和7,以此类推。
- 堆分为最大堆和最小堆,两者的差别在于节点的排序方式上
- 总结:节点位置若为K
- 父节点位置 = K/2
- 左子节点 = 2K
- 右子节点 = 2K + 1
- 每个结点都大于等于它的两个子结点
2、对的实现
- 堆的接口设计
@SuppressWarnings("all")
public class Heap<T extends Comparable<T>> {
//存储堆中的元素
private T[] items;
//记录堆中元素的个数
private int N;
public Heap(int capacity) {
//初始化成员变量
items = (T[]) new Comparable[capacity+1];
N=0;
}
//判断堆中索引i处的元素是否小于索引j处的元素
private boolean less(int i,int j){
return items[i].compareTo(items[j])<0;
}
//交换堆中i索引和j索引处的值
private void exch(int i,int j){
T tmp = items[i];
items[i] = items[j];
items[j] = tmp;
}
//往堆中插入一个元素
public void insert(T t){
//N初始值=0,但0只做索引无其他用途,因此++N从1开始
items[++N] = t;
swim(N);
}
//删除堆中最大的元素,并返回这个最大元素
public T delMax(){
T max = items[1];
//交换索引1处和索引N处的值
exch(1,N);
//删除最后位置上的元素
items[N]=null;
N--;//个数-1
sink(1);
return max;
}
//使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置
private void swim(int k){
//如果已经到了根结点,就不需要循环了
while(k>1){
//比较当前结点和其父结点
if(less(k/2,k)){
//父结点小于当前结点,需要交换
exch(k/2,k);
}
k = k/2;
}
}
//使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
private void sink(int k){
//如果当前已经是最底层了,就不需要循环了
while(2*k<=N){
//找到子结点中的较大者
int max;
if (2*k+1<=N){//存在右子结点
if (less(2*k,2*k+1)){
max = 2*k+1;
}else{
max = 2*k;
}
}else{//不存在右子结点
max = 2*k;
}
//比较当前结点和子结点中的较大者,如果当前结点不小,则结束循环
if (!less(k,max)){
break;
}
//当前结点小,则交换,
exch(k,max);
k = max;
}
}
}
- 元素插入
//往堆中插入一个元素
public void insert(T t){
//N初始值=0,但0只做索引无其他用途,因此++N从1开始
items[++N] = t;
swim(N);
}
//使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置
private void swim(int k){
//如果已经到了根结点,就不需要循环了
while(k>1){
//比较当前结点和其父结点
if(less(k/2,k)){
//父结点小于当前结点,需要交换
exch(k/2,k);
}
k = k/2;
}
}
- 删除元素及删除最大元素
- 当删除掉最大元素后,只需要将最后一个元素放到索引1处,并不断的拿着当前结点a[k]与它的子结点a[2k] 和a[2k+1]中的较大者交换位置,即可完成堆的有序调整。
//删除堆中最大的元素,并返回这个最大元素
public T delMax(){
T max = items[1];
//交换索引1处和索引N处的值
exch(1,N);
//删除最后位置上的元素
items[N]=null;
N--;//个数-1
sink(1);
return max;
}
//使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
private void sink(int k){
//如果当前已经是最底层了,就不需要循环了
while(2*k<=N){
//找到子结点中的较大者
int max;
if (2*k+1<=N){//存在右子结点
if (less(2*k,2*k+1)){
max = 2*k+1;
}else{
max = 2*k;
}
}else{//不存在右子结点
max = 2*k;
}
//比较当前结点和子结点中的较大者,如果当前结点不小,则结束循环
if (!less(k,max)){
break;
}
//当前结点小,则交换,
exch(k,max);
k = max;
}
}
- 当删除掉最大元素后,只需要将最后一个元素放到索引1处,并不断的拿着当前结点a[k]与它的子结点a[2k] 和a[2k+1]中的较大者交换位置,即可完成堆的有序调整。
- 测试
public class heapTest {
public static void main(String[] args) throws Exception {
Heap<String> heap = new Heap<String>(20);
heap.insert("S");
heap.insert("G");
heap.insert("I");
heap.insert("E");
heap.insert("N");
heap.insert("H");
heap.insert("O");
heap.insert("A");
heap.insert("T");
heap.insert("P");
heap.insert("R");
heap.insert("Z");
String del;
while((del=heap.delMax())!=null){
System.out.print(del+",");
}
}
}
3、堆排序
- 什么是堆排序
- 给定一个数组: String[] arr = {“S”,”O”,”R”,”T”,”E”,”X”,”A”,”M”,”P”,”L”,”E”} 请对数组中的字符按从小到大排序。
- 实现步骤:
- 构造堆;
- 得到堆顶元素,这个值就是最大值;
- 交换堆顶元素和数组中的最后一个元素,此时所有元素中的最大元素已经放到合适的位置;
- 对堆进行调整,重新让除了最后一个元素的剩余元素中的最大值放到堆顶;
- 重复2~4这个步骤,直到堆中剩一个元素为止。
- 堆构造过程
- 节点位置若为K,父节点位置 = K/2,左子节点 = 2K,右子节点 = 2K + 1
- 因此索引长度从数组得一半开始
- 堆排序得过程
- 1.将堆顶元素和堆中最后一个元素交换位置;
- 通过对堆顶元素下沉调整堆,把最大的元素放到堆顶(此时最后一个元素不参与堆的调整,因为最大的数据已经到 了数组的最右边;
- 重复1~2步骤,直到堆中剩最后一个元素。
- 堆排序的实现
public class HeapSort {
//对source数组中的数据从小到大排序
public static void sort(Comparable[] source) {
//1.创建一个比原数组大1的数组
Comparable[] heap = new Comparable[source.length + 1];
//2.构造堆
createHeap(source,heap);
//3.堆排序
//3.1定义一个变量,记录heap中未排序的所有元素中最大的索引
int N = heap.length-1;
while(N!=1){
//3.2交换heap中索引1处的元素和N处的元素
exch(heap,1,N);
N--;
//3.3对索引1处的元素在0~N范围内做下沉操作
sink(heap,1,N);
}
//4.heap中的数据已经有序,拷贝到source中
System.arraycopy(heap,1,source,0,source.length);
}
//根据原数组source,构造出堆heap
private static void createHeap(Comparable[] source, Comparable[] heap) {
//1.把source中的数据拷贝到heap中,从heap的1索引处开始填充
System.arraycopy(source,0,heap,1,source.length);
//2.从heap索引的一半处开始倒叙遍历,对得到的每一个元素做下沉操作
for (int i = (heap.length-1)/2; i>0 ; i--) {
sink(heap,i,heap.length-1);
}
}
//判断heap堆中索引i处的元素是否小于索引j处的元素
private static boolean less(Comparable[] heap, int i, int j) {
return heap[i].compareTo(heap[j])<0;
}
//交换heap堆中i索引和j索引处的值
private static void exch(Comparable[] heap, int i, int j) {
Comparable tmp = heap[i];
heap[i] = heap[j];
heap[j] = tmp;
}
//在heap堆中,对target处的元素做下沉,范围是0~range
private static void sink(Comparable[] heap, int target, int range){
//没有子结点了
while (2*target<=range){
//1.找出target结点的两个子结点中的较大值
int max=2*target;
if (2*target+1<=range){
//存在右子结点
if (less(heap,2*target,2*target+1)){
max=2*target+1;
}
}
//2.如果当前结点的值小于子结点中的较大值,则交换
if(less(heap,target,max)){
exch(heap,target,max);
}
//3.更新target的值
target=max;
}
}
}
- 测试
@Test
public void heapSortTest(){
String[] arr = {"S", "O", "R", "T", "E", "X", "A", "M", "P", "L", "C"};
HeapSort.sort(arr);
System.out.println(Arrays.toString(arr));
}
5.1.12、并查集
5.1.13、Huffman树
5.2、图
5.2.1、图形结构的基础
1、图的自我介绍
- 图是由顶点集V和边集E组成,记作G=(V, E) G:Graphics 图形 V:Vertex 顶点 E:Edge 边
- 图的分类
- 边是有方向的则称为有向图
- 边没有方向则称为无向图
- 权图的分类
- 对图中的边赋予具有一定意义的数值(路程、费用等等)的图称为带权图
- 完全图:任意两个顶点之间都存在一条边
稀疏图:有很少的边
稠密图:有很多的边,最稠密的图就是完全图 - 邻接:如果两个顶点在同一条边上,则称它们互为邻接点。即(v, v’) ∈ E,v和v’互为邻接点
- 顶点的度:
- 对于无向图,顶点v的度就是和v相关联的边的条数
- 对于有向图,顶点v的度分为入度和出度
- 路径:非空序列V0 E1 V1 E2 … Vk称为顶点V0到顶点Vk的一条路径。
- 无权图的路径长就是路径包含的边数。
- 有权图的路径长要乘以每条边的权。
- 图的连通性
- 如果从顶点v到顶点v’有路径或从顶点v’到顶点v有路径,则称顶点v和顶点v’是连通的。
- 如果图中任意两个顶点都是连通的,则称该图是连通图。
- 对于有向图,如果图中每一对顶点Vi和Vj是双向连通的,则该图是强连通图。
- 生成树
- 一个连通图G的一个包含所有顶点的极小连通子图T是
- ①T包含G的所有顶点(n个)
- ②T为连通图
- ③T包含的边数最少(n-1条)
- 生成树的性质
- 一个有n个顶点的连通图的生成树有且仅有n-1条边
- 一个连通图的生成树并不唯一
2、图的存储结构
- 图的存储结构分为两种:
- 邻接矩阵
- 邻接表
- 邻接矩阵
- 使用一个V*V的二维数组int[V][V] adj,把索引的值看做是顶点;
- 如果顶点v和顶点w相连,我们只需要将adj[v][w]和adj[w][v]的值设置为1,否则设置为0即可。
- 特点:
- 有多少顶点就生成多大的二维数组,无向图双向索引,如(1,0)和(1,0)表示有边,无向图单向索引(0,1)只表示0指向1
- 邻接矩阵的空间复杂度 O= V^2(V表示顶点数量),不是每个顶点都右边或者都指向别的点,所以邻接表是非常浪费空间的
- 邻接表
- 使用一个大小为V的数组 Queue[V] adj,把索引看做是顶点;
- 每个索引处adj[v]存储了一个队列,该队列中存储的是所有与该顶点相邻的其他顶点
- 特点:
- 无向图:空间复杂度O(n) = V+2E,无向图要考虑回路问题(V:表的列数,E:顶点个数)
- 有向图:空间复杂度O(n) = V+E (V:表的列数,E:顶点个数)
- 详细分析
3、逻辑实现
5.2.2、BFS、DFS、拓扑排序的实现
5.2.3、最小生成树及最短路径
5.2.4、布隆过滤器的实现
5.2.5、跳表原理及实现
六、排序算法集合
6.1、算法分类及复杂度
6.2、冒泡排序
1、排序思想
- 类似于水中冒泡,较大的数往上泡,较小的数在后面,进而形成一串从小到大往水面冒出的排列
- 冒泡排序特点是在无序序列中,前后两个元素作对比,大的往后,小的往前,在第一轮比较中能都列出最大的元素,多次的比较之后形成一个有序序列的过程
2、算法描述
- 比较相邻的两个元素,如果后面一个比较前面一个大,就交换位置。
- 第1轮:比较前两个数,确定顺序,再比较第2个数和第3个数,再确定顺序,再比较第3个数和第4个数,一直比较到最后一个数,最后将最大的一个数排到最后
- 第2轮:两两一组比较大小,将第二大的数排到倒数第二个位置
- 第3轮:……将第三大的数排到倒数第三个位置
………
n. 第n-1轮:比较最后一组大小顺序不同的数并确定顺序
3、实现过程
4、代码逻辑
4.1、冒泡排序
- 待改善:
- 不论数组元素大小顺序如何都要经历n-1轮排序
- 在有序数组的排序上,还能优化
测试class BubbleSort_01{
/**
* 冒泡排序初级
* @param array
*/
public void Sort(int[] array){
//进行n-1轮比较排序,每比较一轮,就能确定一个最大值,就能少比较一个数
for (int end =array.length -1;end >0; end--){
//从位置1开始,开始第一轮排序
for(int begin = 1; begin <= end; begin++){
//比较第1个数和前一个数的大小,并交换位置
if(array[begin] < array[begin -1]){
//位置交换
int tmp = array[begin];
array[begin] = array[begin -1];
array[begin - 1] = tmp;
}
}
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
}
}
@Test
public void SortTest_03(){
BubbleSort_02 sort = new BubbleSort_02();
int[] array = {56,1,34,12,9,22,40};
sort.Sort(array);
}
4.2、冒泡排序优化1
- 数组完全有序的情况不是常见情况,还能再做优化
//冒泡排序2
class BubbleSort_02{
/**
* 冒泡排序优化全部有序数组
* @param array
*/
public void Sort(int[] array){
//进行n-1轮比较排序,每比较一轮,就能确定一个最大值,就能少比较一个数
for (int end =array.length -1;end >0; end--){
//预先假设给定的数组是有序的
boolean sorted = true;
//从位置1开始,开始第一轮排序
for(int begin = 1; begin <= end; begin++){
//比较第1个数和前一个数的大小,并交换位置
if(array[begin] < array[begin -1]){
//位置交换
int tmp = array[begin];
array[begin] = array[begin -1];
array[begin - 1] = tmp;
sorted = false;
}
}
if(sorted)break;
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
}
}
测试
4.3、冒泡排序优化2
- 在尾部局部有序的情况下,可以记录最后一次交换的位置,减少排序次数
//冒泡排序3
class BubbleSort_03{
/**
* 冒泡排序优化尾部局部有序数组
* @param array
*/
public void Sort(int[] array){
//进行n-1轮比较排序,每比较一轮,就能确定一个最大值,就能少比较一个数
for (int end =array.length -1;end >0; end--){
//定义记录最后一次交换位置的变量,赋值1时保证了完全有序的情况下第一轮结束整个冒泡排序就结束了
int sortIndex =1;
//从位置1开始,开始第一轮排序
for(int begin = 1; begin <= end; begin++){
//比较第1个数和前一个数的大小,并交换位置
if(array[begin] < array[begin -1]){
//位置交换
int tmp = array[begin];
array[begin] = array[begin -1];
array[begin - 1] = tmp;
//位置标记
sortIndex = begin;
}
}
end =sortIndex;
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
}
}
测试
6.3、选择排序
1、排序思想
- 选择排序(Select Sort) 是直观的排序,从序列中,通过确定一个 Key 最大或最小值,再从待排序的的数中找出最大或最小的交换到对应位置。再选择次之。双重循环时间复杂度为 O(n^2)
- 从无序序列的一端开始,找到最大或最小的一个元素和第一个元素比较,比第一个元素小或大就交换位置,依次循环知道序列有序
2、算法描述
- 第一轮:第一次遍历 n-1 个数找到最大或最小的和最后一个数交换。
- 第二轮:从下一个数开始遍历 n-2 个数,找到最大或最小的数和倒数第二个数交换。
第三轮:继续从下下一个数开始遍历n-3个数,找到最大或最小的数和倒数第三个数交换。
……..n. 重复以上操作直到第 n-1 次遍历最小的数和第 n-1 个数交换,排序完成。
3、实现过程
4、代码逻辑
class SelectSort_1{
public void Sort(int[] array){
//
for (int end = array.length - 1; end > 0; end--) {
//定义最大值的索引
int maxIndex = 0;
//开始第一轮的排序
for (int begin = 1; begin <= end; begin++) {
//第begin个元素大于最大值时,begin就是最大值
if(array[maxIndex] <= array[begin]){
maxIndex = begin;
}
}
//遍历到数组中的最大值后,与数组的最后一个元素交换位置
int tmp = array[maxIndex];
array[maxIndex] = array[end];
array[end] = tmp;
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
}
- 测试
@Test
public void SortTest(){
SelectSort_1 sort = new SelectSort_1();
int[] array = {5,9,12,50,33,27,41,1};
sort.Sort(array);
}
6.4、堆排序
1、排序思想
- 堆排序实际上时选择排序的优化,在堆中,找出最大或最小值,放在最后的位置,但堆的优点是利用的二叉树遍历的特点,复杂度为O(n) = nlogn
- 堆排序是对选择排序的优化,时间复杂度有所提高
2、算法描述
- ① 对序列进行原地建堆(heapify)
- ② 重复执行以下操作(依次取出堆中最大值,并删除),直到堆的元素数量为 1
- 交换堆顶元素与堆尾元素(把最大值放到最后面)
- 堆的元素数量减 1(不管最后已经放到最后的最大值)
- 对 0 位置进行 1 次 adjustHeap操作
3、实现过程
4、逻辑代码
public static void sort(int[] array){
//1.构建大顶堆
for(int i=array.length/2-1;i>=0;i--){
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(array,i,array.length);
}
//2.调整堆结构+交换堆顶元素与末尾元素
for(int j=array.length-1;j>0;j--){
swap(array,0,j);//将堆顶元素与末尾元素进行交换
adjustHeap(array,0,j);//重新对堆进行调整
}
}
/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
* @param arr
* @param i
* @param length
*/
public static void adjustHeap(int []arr,int i,int length){
int temp = arr[i];//先取出当前元素i
for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始
if(k+1<length && arr[k]<arr[k+1]){//如果左子结点小于右子结点,k指向右子结点
k++;
}
if(arr[k] >temp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[i] = arr[k];
i = k;
}else{
break;
}
}
arr[i] = temp;//将temp值放到最终的位置
}
/**
* 交换元素
* @param arr
* @param a
* @param b
*/
public static void swap(int []arr,int a ,int b){
int temp=arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
测试
@Test
public void HeapTest(){
int []arr = {9,12,3,7,21,60,36,49,55};
sort(arr);
System.out.println(Arrays.toString(arr));
}
6.5、快速排序
1、排序思想
- 快速排序是由冒泡排序改进而得到的,是一种分区交换排序方法。一趟快速排序采用从两头向中间扫描的方法,同时交换与基准记录逆序的记录。
- 分区:即将比较大的问题,分解成为比较小的问题,再对这些比较小的问题进行求解,然后再将小问题的结果合并在一起,成为最终的结果。这也是快速排序的核心思想。
- 归并:归并是将比较小的已有结果的问题按照一定的规则结合在一起得到一个比较大的问题的结果,这其中有多路归并的概念,二路归并是其中一种多路归并,这里的多就是二,在快速排序中,算做是三路归并。
- 递归:递归的思想,是将数据量比较大的问题,一层一层的拨开,知道体量小到很容易得到结果的时候,然后再回归知道最终的结果,在快速排序中递归不能算作是核心的思想,但是代码中一般会用到。
2、算法描述
- 首先是在待排序数组中选择一个基准数据,一般就是选择第一个元素;
- 然后将数组中比基准数据小的数据移动到基准数据的左边(这里是从小到大排序,从大到小的排序正好相反),比基准数据大的数据移动到基准数据的右边;(分区)
- 然后将基准数据左边的数据看作一个新的数组,将基准数据右边的数据看作是一个新的数组。分别对这两个数据进行快速排序(递归)
- 直到所有的子数组都是有序的,那么就可以保证整个数组是有序的。
- 也就是基准数据的左边都比基准数据小,右边都比基准数据大,且都是有序的,所以合并起来就是有序的(归并)
3、实现过程
4、代码逻辑
public static void quickSort(int[] array,int low,int high){
int i,j,temp,t;
if(low>high){
return;
}
i=low;
j=high;
//temp就是基准位
temp = array[low];
while (i<j) {
//先看右边,依次往左递减
while (temp<=array[j]&&i<j) {
j--;
}
//再看左边,依次往右递增
while (temp>=array[i]&&i<j) {
i++;
}
//如果满足条件则交换
if (i<j) {
t = array[j];
array[j] = array[i];
array[i] = t;
}
}
//最后将基准为与i和j相等位置的数字交换
array[low] = array[i];
array[i] = temp;
//递归调用左半数组
quickSort(array, low, j-1);
//递归调用右半数组
quickSort(array, j+1, high);
}
- 测试
@Test
public void SortTest(){
int[] array = {10,7,4,62,3,2,1,8,9,19};
quickSort(array, 0, array.length-1);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
}
6.6、插入排序
1、排序思想
- 通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
- 在序列中找出一个元素,和前面的所有元素作比较,小于那个元素就插入到那个元素的前面
2、算法描述
- 插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5;
3、实现过程
4、逻辑代码
/**
* 插入排序
* @param array
*/
public static void insertSort(int[] arr) {
//从i=1 开始,因为单独一个元素arr[0]是有序的;
for(int i = 1;i < arr.length-1;i++) {
//从无序数列中取出一个元素赋值给temp
int temp = arr[i];
int t = i-1;
//不断往前寻找,直到找到比temp小的值或者t小于0为止
while(t >= 0 && arr[t] > temp) {
arr[t + 1] = arr[t];
t--;
}
//将temp插在其之后
arr[t+1] = temp;
}
System.out.println(Arrays.toString(arr));
}
- 测试
@Test
public void SortTest(){
int[] array = {19,3,8,27,4,33,5,10,1,44};
insertSort(array);
}
6.7、希尔排序
1、排序思想
- 先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录”基本有序”时,再对全体记录进行依次直接插入排序。
- 是插入排序的优化版,在序列中按增量分组,在分组中先进行排序,每个分组排序完之后在进行最后的插入排序。
2、算法描述
- 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
- 按增量序列个数 ,形成不同的分组,在每个分组中再继续插入排序;
- 当第一轮增量排序完成之后,再一次缩小增量重新分组,再次继续插入排序,直到完成。
- 增量d 的范围: 1<= d < 待排序数组的长度 (d 需为 int 值)
- 增量的取值: 一般的初次取序列(数组)的一半为增量,以后每次减半,直到增量为1。
- 第一个增量=数组的长度/2,
- 第二个增量= 第一个增量/2,
- 第三个增量=第二个增量/2,
- 以此类推,最后一个增量=1。
3、实现过程
4、代码实现
- “三层循环+if” _———_ for(for(while(if)))
/**
* 希尔排序
* @param array
*/
public static void shellSort(int[] array){
//定义数组长度
int length = array.length;
//定义中间遍历
int temp;
//按增量升序遍历
for (int step = length / 2; step >= 1; step /= 2) {
//缩小增量再次升序遍历
for (int i = step; i < length; i++) {
//按增量进行分组
temp = array[i];
//找到分组中的每一个元素
int j = i - step;
//进行最后一轮的插入排序
while (j >= 0 && array[j] > temp) {
array[j + step] = array[j];
j -= step;
}
array[j + step] = temp;
}
}
System.out.println(Arrays.toString(array));
}
- 测试
@Test
public void SortTest(){
int[] array = {32,5,22,3,12,9,44,1,50};
shellSort(array);
}
6.8、归并排序
1、排序思想
- 归并排序利用了归并的思想实现的排序方法,该算法采用经典的分治策略,先将序列无限对半分割成无穷个组,直到没有组只有一个元素为止,再将每个组两两有序合并,直到所有组合并成为一个有序序列
2、算法描述
- 不断地将当前序列平均分割成 2 个子序列直到不能再分割(序列中只剩 1 个元素)
- 不断地将 2 个子序列合并成一个有序序列直到最终只剩下 1 个有序序列
- 将两个序列合并的思路为:左序列和右序列的中元素挨个比较,将较小的放入新序列中,最后新序列中的元素必然升序。
- li,ri 分别代表指向左、右序列的元素索引,ai 为新序列(合并后的序列)的元素索引;【li】代表左序列 li 位置的元素,【ri】代表右序列 ri 位置的元素,【ai】为新序列 ai 位置的元素
- 第一轮:【li】 < 【ri】,【li】放入新数组,【ai】=【li】,li++; ai++;
第二轮:【li】 > 【ri】,【ri】放入新数组,【ai】=【ri】,ri++; ai++;
第三轮:【li】 < 【ri】,【li】放入新数组,【ai】=【li】,li++; ai++;
第四轮:左序列已经遍历完毕,直接将右序列剩余元素放入新序列,得到新序列(升序)。
- 第一轮:【li】 < 【ri】,【li】放入新数组,【ai】=【li】,li++; ai++;
- 时间复杂度O(n) = nlogn
- 空间复杂度T(n) = n
3、实现过程
- 实现排序的整个过程
- 实现最后归并成为一个有序序列的最后一步
4、代码实现
- 归并排序分为三个过程
- 分组过程
/**
*分组过程
* @param arr
* @param left
* @param right
* @param temp
*/
private static void sort(int[] arr,int left,int right,int []temp){
if(left<right){
int mid = (left+right)/2;
sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
}
}
- 合并过程
/**
*合并过程
* @param arr
* @param left
* @param mid
* @param right
* @param temp
*/
private static void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left;//左序列指针
int j = mid+1;//右序列指针
int t = 0;//临时数组指针
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
//将temp中的元素全部拷贝到原数组中
while(left <= right){
arr[left++] = temp[t++];
}
}
- 排序过程
**
*归并排序
* @param array
*/
public static void sort(int []array){
int []temp = new int[array.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
sort(array,0,array.length-1,temp);
}
- 测试
@Test
public void SortTest(){
int []array = {34,12,3,50,9,28,5,44,15};
sort(array);
System.out.println(Arrays.toString(array));
}
七、算法策略
7.1、递归分治策略
7.1.1、递归
- 递归:简言之就是定义某个方法自己直接或间接调用自己
- 方法的调用过程
- 递归=递推+回归;
- 递推:问题向一极推进,这一过程叫做递推;这一过程相当于压栈。
- 回归:问题逐一解决,最后回到原问题,这一过程叫做回归。这一过程相当于弹栈。
- 栈空间会将调用的函数依次入栈,一般来说最先入栈的是
main
,下图中的test1
虽然被调用了,但是没有执行操作,编译器会忽略它,test2
调用了test3
,所以test2
、test3
依次入栈。
- 方法递归过程
main
先入栈,sum(4)
、sum(3)
、sum(2)
、sum(1)
由于递归调用依次入栈,可见空间复杂度为 O(n);- 执行过程
7.1.2、分而治之
- “分而治之”( Divide and conquer)方法(又称“分治术”) ,是有效算法设计中普遍采用的一种技术。
- 所谓“分而治之” 就是把一个复杂的算法问题按一定的“分解”方法分为等价的规模较小的若干部分,然后逐个解决的过程
- 基本思想
- 把它分成两个或多个更小的问题;
- 分别解决每个小问题;
- 把各小问题的解答组合起来,即可得到原问题的解答。
- 分治的核心
- 分解
- 解决
- 合并
- 适用场景
- 该问题的规模缩小到一定的程度就可以容易地解决
- 利用该问题分解出的子问题的解可以合并为该问题的解;
- 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
7.1.3、实例策略分析
1、递归分治算法适用案例
- 求和问题
- 二分查找
- 大整数乘法
- Strassen矩阵乘法
- 残缺棋盘
- 归并/快速排序
- 线性时间选择
- 汉诺塔等
2、案例分析
- 求和问题
- 递归做法
/**
* 递归求和
* @param n
* @return
*/
public static int sum_01(int n){
if(n <= 1) return n;
return sum_01(n - 1) + n;
}
- 递归做法
- 循环做法
/**
* 循环求和
* @param n
* @return
*/
public static int sum_02(int n){
int result = 0;
for (int i = 0; i <= n; i++) {
result += i;
}
return result;
}
- 求和公式
/**
* 公式求和
* @param n
* @return
*/
public static int sum_03(int n) {
if (n <= 1) return n;
return (1 + n) * n >> 1;
}
- 测试
@Test
public void sum_01Test(){
System.out.println("sum_01:"+sum_01(100));
System.out.println("sum_02:"+sum_01(100));
System.out.println("sum_03:"+sum_01(100));
}
- 斐波那契
- 斐波那契三种实现 ```java /**
- @author hguo
- @date2021/6/28 8:50
- @title 斐波那契数列
/
public class Fibonacci {
/*
- 特征方程求解斐波那契
- @param n
- @return
*/
public int fib_01(int n){
//获取数学格式的根号5
double c = Math.sqrt(5);
//特征方程
return (int)((Math.pow((1+c)/2,n) - Math.pow((1-c)/2,n))/c);
}
@Test
public void fib_01Test(){
int n = 40;
TimeTool.check(“特征方程”, new TimeTool.Task() {
}); }@Override
public void execute() {
System.out.println(fib_01(n));
}
/**
* 滚动数组求解斐波那契,数组可能发生溢出
* @param n
* @return
*/
public int fib_02(int n) {
if (n <= 1) return n;
int first = 0;
int second = 1;
for (int i = 0; i < n- 1; i++) {
int sum = first +second;
first = second ;
second = sum;
}
return second;
}
@Test
public void fib_02Test(){
int n = 40;
TimeTool.check("滚动数组", new TimeTool.Task() {
@Override
public void execute() {
System.out.println(fib_02(n));
}
});
}
/**
* 递归优化求解斐波那契
* @param n
* @return
*/
public int fib_03(int n) {
if(n <= 2) return 1;
int[] array = new int[n + 1];
array[2] = array[1] = 1;
for (int i = 3; i <= n; i++) {
array[i] = array[i - 1] + array[i - 2];
}
return array[n];
}
@Test
public void fib_03Test(){
int n = 40;
TimeTool.check("去递归法", new TimeTool.Task() {
@Override
public void execute() {
System.out.println(fib_03(n));
}
});
}
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210628152149466.png#crop=0&crop=0&crop=1&crop=1&id=cILyw&originHeight=549&originWidth=640&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- **汉诺塔问题** ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210628152324653.png#crop=0&crop=0&crop=1&crop=1&id=g8yPJ&originHeight=484&originWidth=1239&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
```java
/**
* @author hguo
* @date2021/6/28 10:35
* @title 汉诺塔
*/
public class Hanoi {
/**
* 将第i个盘从form移动到to的过程
* @param i
* @param from
* @param to
*/
void move(int i, String from, String to) {
System.out.println(i + "号盘子: " + from + "->" + to);
}
/**
* 将n个盘从p1移到p3的过程
* @param n 圆盘数
* @param p1 柱子p1
* @param p2 柱子p1
* @param p3 柱子p1
*/
void hanoi(int n, String p1, String p2, String p3) {
if (n <= 1) {
move(n, p1, p3);
return;
}
hanoi(n - 1, p1, p3, p2); // 将 n – 1 个盘子从 p1 移动到 p2
move(n, p1, p3); // 将编号为 n 的盘子从 p1 移动到 p3
hanoi(n - 1, p2, p1, p3); // 将 n – 1 个盘子从 p2 移动到 p3
}
@Test
public void HanoiTest(){
new Hanoi().hanoi(4,"A","B","C");
}
}