数据结构与算法
一、数据结构
1.基础概念
1.1 数据结构与算法
数据结构是计算机存储、组织数据的方式。
数据结构是相互之间存在一种或多种特定关系的数据元素的集合,即带
“结构”的数据元素的集合。“结构”就是指数据元素之间存在的关系,分为逻
辑结构和存储结构。
通常情况下,精心选择的数据结构可以带来更高的运行或存储效率。数据
结构往往同高效的检索算法和索引技术有关。
算法是操作数据的方法,即如何操作数据效率更高,更节省资源。
数据结构和算法看起来是两个东西,但是两者在使用时是相辅相成的。数
据结构是为算法服务的,而算法要作用在特定的数据结构之上,因此,我
们无法孤立数据结构来讲算法,也无法孤立算法来讲数据结构。
只看上面概念大家可能会觉得抽象、无法理解,举一个简单的例子:你买
了很多书,如何去放置,是放在箱子里还是放到书架上?这就是数据结构
的范畴,关键点在于选取什么样的结构来存储。至于你放书或摆书的时候
是简单的堆放在一起还是先将书分类,或者按书的首字母排序摆放,这就
是算法放到范畴了,关键点在于要包装更有效率更节省空间。
1.2 逻辑结构
逻辑结构是指数据对象中数据元素之间的相互关系。包括集合结构、线
性结构、树形结构、图形结构。
集合结构:集合结构中的元素关系,除了同属于一个集合这个关系以
外,再无其他关系。线性结构:类似于线性关系,也就是说,线性结构中的数据元素之间是
一对一的关系。
树形结构:树形结构中的数据元素之间存在一对多的关系。
图形结构:数据元素之间是多对多的关系。
具体可参考下图:
1.3 物理结构
物理结构是指数据在计算机中的存储形式。实际上就是如何把数据元素
存储到计算机的存储器中。
物理结构分为两种,一种是顺序存储结构,一种是链式存储结构,具体如
下图:1.4 数据结构分类
数据结构可以分成两大类:线性结构和非线性结构。
线性结构也可称为线性表,管理数据时将数据排成像一条长线一样的结
构,数组,链表,栈,队列都是线性表结构;
非线性结构中数据则不再是简单的前后关系,如树、堆、图,可参考下
图:注意:线性表结构,数组,链表,栈,队列等是学习其他数据结构和算
法的基础。
2.数组
2.1 数组概念
数组用一块连续的内存空间,来存储一组具有相同类型的数据。
2.2 元素访问
数组通过下标来访问元素。下标从0开始,到len-1。2.3 数组特点
高效的随机访问;低效插入和删除。
注意:在某些特殊场景下,我们并不一定非得追求数组中数据的连续性。
上图中a3,a4,a5,a6会被搬移两次,效率很低。为了提高效率,我们可以先记
录下已经删除的数据。每次的删除操作并不真正地搬移数据,只是记录下
数据已经被删除。当数组没有更多空间存储数据时,我们再触发执行一次
真正的删除操作,这样就大大减少了删除操作导致的数据搬移。JVM中标记
清除垃圾回收算法的核心思想就是如此。
- 高效随机访问:数组通过下标访问元素,底层通过首地址和寻址公式能够快
速找到想要访问的元素;
- 低效插入删除:插入和删除时为了保证数组的连续性,需要移动数组中多个
元素。
1
22.4 数组应用
针对数组类型,很多语言都提供了容器类,比如 Java 中的 ArrayList、
C++ STL中的 vector底层都是借助数组实现的功能。
到底是选择ArrayList还是直接使用数组,ArrayList的优势有哪些?
ArrayList将借助数组完成的操作细节封装了起来,用起来更简单
支持动态扩容
2.5 ArrayList源码分析
ArrayList通过无参构造创建容器并添加元素
ArrayList测试代码:
/**
* @ClassName: ArrayListDemo
* @author shaoyb
* @date 2020年12月14日
* @Description: ArrayList 测试代码
*/
public class ArrayListDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 向集合list中存入11个字符串元素a,b,c,d,e,f,g,
h,i,j,k
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
list.add("f");
list.add("g");
list.add("h");
list.add("i");
list.add("j");
// 往集合中添加 第11个元素
list.add("k");
// 集合中获取指定位置元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24底层源码分析:
经过上述分析可知:
1. 采用默认的构造函数创建ArrayList,然后往里添加元素,第一次添加数
组被扩容到 10 的大小,之后添加元素都不会在扩容,直到第 11 次添
加时会先扩容 1.5 倍然后完成添加。此后如果需要扩容都是 1.5 倍取
整,但是扩容最大值是Integer.MAX_VALUE;
2. 每次扩容时都会创建一个新数组,然后将原数组中的数据搬移到新的
数组中。
ArrayList获取元素
String s = list.get(0);
System.out.println(s);
}
}
25
26
27
282.6 自定义ArrayList
自定义一个MyArrayList类,完成类似ArrayList的功能,具体代码如下:
1
/**
*
* @ClassName: MyArrayList
* @author shaoyb
* @date 2020年12月10日
* @Description: 自定义顺序表
*
* @param <T> 任意 存储到集合中的 类型
*/
public class MyArrayList<T> {
private int AMOUNT = 10; //初始容量
private T[] myList; //底层依赖数组
private int size; //集合元素个数
public MyArrayList(){
initList();
}
//初始化
public void initList(){
myList = (T[]) new Object[AMOUNT];
size = 0;
}
//判断是否为空
public boolean listEmpty(){
if(size == 0){
return true;
}
return false;
}
//清理元素
public boolean clearList() {
myList = null;
size = 0;
return true;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37}
//根据下标获取元素
public T get(int i) {
if(i < 0 || i >= size){
throw new ArrayIndexOutOfBoundsException();
}
return myList[i];
}
//添加元素
public void add(T a){
add(size,a);
}
//添加元素到指定位置,如果容量不够则扩容
public void add(int i,T a){
if(i < 0 || i > size) {
throw new ArrayIndexOutOfBoundsException();
}
// 数组已满,则扩容
if(i == myList.length) {
largeList();
}
myList[i] = a;
size++;
}
// 扩容1.5倍
private void largeList(){
myList = Arrays.copyOf(myList, size + size / 2);
}
// 设置指定位置元素值
public T set(T a,int i){
if(i < 0 || i >= size){
throw new ArrayIndexOutOfBoundsException();
}
T old = myList[i];
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78myList[i] = a;
return old;
}
//获取长度
public int size() {
return size;
}
//删除指定位置元素
public T delete(int i){
if(i < 0 || i >= size){
throw new ArrayIndexOutOfBoundsException();
}
T old = myList[i];
// 整体前移 i位置后面的值
for(int k = i; k < size; k++){
myList[k] = myList[k+1];
}
// 集合元素个数 减一
size--;
return old;
}
@Override
public String toString(){
String s = "[";
for(int i = 0; i < size; i++){
s = s + myList[i];
if(i != size - 1)
s = s + " ,";
}
s += "]";
return s;
}
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
1193.链表
链表(Linked list)是一种物理存储单元上非连续、非顺序的存储结
构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由
一系列结点组成,结点包括两部分:一个是存储数据元素的数据域,另
一个是存储下一个结点地址的指针域。
public static void main(String[] args) {
// 初始化集合
List<String> list = new ArrayList<String>();
// 向集合中添加11个元素
list.add("briup01");
list.add("briup02");
System.out.println(list);
//自定义集合测试
MyArrayList<String> list2 = new MyArrayList<>();
boolean flag = list2.listEmpty();
System.out.println("flag: " + flag);
list2.add("briup03");
list2.add("briup04");
//清理容器
//list2.clearList();
//获取指定索引上元素值
String s = list2.get(1);
System.out.println("s:" + s);
System.out.println(list2);
}
}
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
1483.1 链表存储结构
相比数组,链表稍微复杂一点。这两个是最基础、非常常用的两个数据结
构,我们常常会放到一块儿对比学习。下面为两者的区别:
数组:需要一块连续的存储空间,对内存要求高,比如我们要申请一个
1G 的数组,如果内存中没有连续的足够大的存储空间则会申请失败。
链表:并不需要一块连续的内存空间,它通过指针将一组零散的内存块
串联起来使用,所以如果我们申请一个1G 大小的链表,只要内存剩余
的可用空间大于 1G,便不会出现问题。
两者存储结构可参考下图:
3.2 链表分类
链表可大致分为四类:单链表、循环链表、双向链表、双向循环链表。
3.2.1 单链表
单链表是链表最基本的结构,它通过指针将一组零散的内存块串联在一
起。其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每
个链表的结点不仅要存储数据,还要记录下一个结点的地址。具体见下
图:链表也支持数据的查找、插入和删除操作,相对于数组,链表的插入与删
除效率高,而查询效率偏低。
3.2.2 循环链表
循环链表是一种特殊的单链表,特殊之处在于尾结点:单链表的尾结点指
针指向空地址,表示这就是最后的结点,而循环链表的尾结点指针是指向
链表的头结点。具体可见下图:
和单链表相比,循环链表的优点是从链尾到链头比较方便。3.2.3 双向链表
单链表只支持一个方向,从头结点遍历到尾结点。双向链表,顾名思义,
它支持两个方向,每个结点包含两个指针域,一个指向前一个结点,另一
个指向后一个结点。
双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,
双向链表要比单链表占用更多的内存空间。虽然浪费了存储空间,但支持
双向遍历,操作更灵活,更高效。
3.2.4 双向循环链表
双向循环链表是在双向链表的基础上,修改尾结点next指针域的值,让其
指向头结点,修改头结点pre指针域的值,让其指向尾结点。
总结:数组的优势是随机查询速度快,但是增删慢;链表的优势是增删
快,但随机查询慢。
3.3 链表应用
Java中基于链表实现的集合为LinkedList。
LinkedList底层借助双向链表实现功能,接下来我们可以看下该类的具体
使用与源码分析,如下图:
测试代码:
import java.util.LinkedList;
import java.util.List;
/**
1
2
3
4LinkedList集合的构建及元素添加:
* 链表源码分析测试
*/
public class LinkedListDemo {
public static void main(String[] args) {
//构建集合
List<String> list = new LinkedList<String>();
//向集合中添加数据
list.add("test");
list.add("briup");
//输出集合中所有元素
System.out.println(list);
//从集合中获取指定位置元素
String s = list.get(1);
System.out.println("s: " + s);
}
}
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//LinkedList源码实现【已添加注释】
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable,
java.io.Serializable
{
transient int size = 0;
//头结点
transient Node<E> first;
//尾结点
transient Node<E> last;
//无参构造器
public LinkedList() {
}
//往集合中添加一个元素
public boolean add(E e) {
linkLast(e);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18LinkedList中元素的获取:
return true;
}
//核心代码:添加元素
void linkLast(E e) {
//获取最后结点
final Node<E> l = last;
//构建一个新结点
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
//判断当前集合是否为空,然后添加结点
if (l == null) //最后结点为null,即集合为空,设置新结
点为first结点即可
first = newNode;
else //最后结点存在,则集合不空,直接在last
后面添加新结点即可
l.next = newNode;
size++;
modCount++;
}
//静态static结点内部类的定义
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
//构建双向链表结点,传递前置结点、数据域值、后置结点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//获取指定位置元素
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
1
2
3
4
5
63.4 自定义链表
自定义单链表源码:
//索引越界则抛出异常
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new
IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//判断索引是否越界
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
//越界异常信息提醒
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
//遍历链表,获取指定位置元素并返回
Node<E> node(int index) {
// assert isElementIndex(index);
//如果索引小于 链表长度的一半
if (index < (size >> 1)) {
//从头结点开始遍历
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//从尾结点开始遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
1*
* @ClassName: MyLinkedList
* @author shaoyb
* @date 2020年12月14日
* @Description: 自定义单链表
*
* @param <T> 任意类型
*/
public class MyLinkedList<T> {
//【单链表】结点类
private class Node {
private T t; // 数据域
private Node next; // 指针域指向下一个结点
public Node(T t) {
this(t, null);
}
public Node(T t, Node next) {
this.t = t;
this.next = next;
}
}
private Node head; // 头结点
private int size; // 链表元素个数
// 构造函数
public MyLinkedList() {
this.head = null;
this.size = 0;
}
// 获取链表元素的个数
public int getSize() {
return this.size;
}
// 判断链表是否为空
public boolean isEmpty() {
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42return this.size == 0;
}
// 链表头部添加元素
public void addFirst(T t) {
Node node = new Node(t); // 结点对象
node.next = this.head;
this.head = node;
// this.head = new Node(e,head);等价上述代码
this.size++;
}
// 向链表尾部插入元素
public void addLast(T t) {
this.add(t, this.size);
}
// 向链表中间插入元素
public void add(T t, int index) {
// 索引判断是否有效
if (index < 0 || index > size) {
throw new IllegalArgumentException("index is
error");
}
// 如果往表头插入,则调用现成方法
if (index == 0) {
this.addFirst(t);
return;
}
// 找到要插入结点的前一个结点
Node preNode = this.head;
for (int i = 0; i < index - 1; i++) {
preNode = preNode.next;
}
// 定义新结点,然后到preNode结点后面
Node node = new Node(t);
// 关键代码
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82node.next = preNode.next; // 新结点的后继为 原
preNode后结点
preNode.next = node; // preNode结点后 插入
新结点
this.size++; // 结点数量加1
}
// 删除链表元素
public void remove(T t) {
if (head == null) {
System.out.println("无元素可删除");
return;
}
// 要删除的元素与头结点的元素相同
while (head != null && head.t.equals(t)) {
head = head.next;
this.size--;
}
/**
* 上面已经对头结点判别是否要进行删除 所以要对头结点的下
一个结点进行判别
*/
Node cur = this.head;
while (cur != null && cur.next != null) {
if (cur.next.t.equals(t)) {
this.size--;
cur.next = cur.next.next;
} else
cur = cur.next;
}
}
// 删除链表第一个元素
public T removeFirst() {
if (this.head == null) {
System.out.println("无元素可删除");
return null;
}
Node delNode = this.head;
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120this.head = this.head.next;
delNode.next = null;
this.size--;
return delNode.t;
}
// 删除链表的最后一个元素
public T removeLast() {
if (this.head == null) {
System.out.println("无元素可删除");
return null;
}
// 只有一个元素
if (this.getSize() == 1) {
return this.removeFirst();
}
// 链表存在多个结点
Node secLast = this.head; // 定义倒数第二个结点
while (secLast.next.next != null) {
secLast = secLast.next;
}
//记录最后结点中存储的值
T t = secLast.next.t;
secLast.next = null;
this.size--;
return t;
}
// 链表中是否包含某个元素
public boolean contains(T t) {
Node cur = this.head;
while (cur != null) {
if (cur.t.equals(t)) {
return true;
}else
cur = cur.next;
}
return false;
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161自定义双链表源码:
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
Node cur = this.head;
while (cur != null) {
sb.append(cur.t + "->");
cur = cur.next;
}
sb.append("NULL");
return sb.toString();
}
public static void main(String[] args) {
MyLinkedList<Integer> MyLinkedList = new
MyLinkedList<>();
for (int i = 0; i < 10; i++) {
MyLinkedList.addFirst(i);
System.out.println(MyLinkedList);
}
MyLinkedList.addLast(33);
MyLinkedList.addFirst(33);
MyLinkedList.add(33, 5);
System.out.println(MyLinkedList);
MyLinkedList.remove(33);
System.out.println(MyLinkedList);
System.out.println("删除第一个元素:" +
MyLinkedList.removeFirst());
System.out.println(MyLinkedList);
System.out.println("删除最后一个元素:" +
MyLinkedList.removeLast());
System.out.println(MyLinkedList);
}
}
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
/**
1*
* @ClassName: DuLinkList
* @author shaoyb
* @date 2020年12月14日
* @Description: 自定义双向链表
*
* @param <T> 任意类型
*/
public class DuLinkList<T> {
// 定义一个内部类结点
private class Node {
private T data; //数据域
private Node prev; //指向上一个结点的引用
private Node next; // 指向下一个结点的引用
public Node(T data, Node prev, Node next) {
this.data = data;
this.prev = prev;
this.next = next;
}
}
// 头结点
private Node header;
// 尾结点
private Node tail;
// 链表中结点总数
private int size;
// 创建空链表
public DuLinkList() {
header = null;
tail = null;
}
// 以指定数据元素来创建链表
public DuLinkList(T element) {
header = new Node(element, null, null);
tail = header;
size++;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42// 返回链表长度
public int length() {
return size;
}
// 获取索引为index结点的元素
public T get(int index) {
return getNodeByIndex(index).data;
}
// 根据索引index获取指定位置的结点
private Node getNodeByIndex(int index) {
if (index < 0 || index > size - 1) {
throw new IndexOutOfBoundsException("双向链表
越界!");
}
// 判断索引在链表的前半部分还是后半部分
if (index <= size / 2) {
// 从header结点开始
Node current = header;
int i = 0;
while(i < index) {
current = current.next;
i++;
}
return current;
} else {
// 从tail结点开始
Node current = tail;
int i = size - 1;
while(i > index) {
current = current.prev;
i--;
}
return current;
}
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82}
// 查找链式线性表指定元素的索引
public int locate(T element) {
// 从头结点开始搜索
Node current = header;
for (int i = 0; i < size; i++) {
if(current.data.equals(element))
return i;
current = current.next;
}
return -1;
}
// 插入一个元素
public void insert(T element, int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("双向链表
索引越界!");
}
if (header == null) {
add(element);
} else {
// index为0,也就是在链表头插入
if (index == 0) {
addAtHeader(element);
} else {
// 核心代码
// 获取插入点的前一个结点
Node prev = getNodeByIndex(index - 1);
// 获取插入点的结点
Node next = prev.next;
// 让新结点的next引用指向next结点,prev引用指
向prev结点
Node newNode = new Node(element, prev,
next);
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120// 让prev的next指向新结点
prev.next = newNode;
// 让prev的下一个结点的prev指向新的结点
next.prev = newNode;
size++;
}
}
}
// 采用尾插法为链表添加新结点
public void add(T element) {
if (header == null) {
header = new Node(element, null, null);
tail = header;
} else {
// 创建新结点,新结点的prev引用指向原tail结点
Node newNode = new Node(element, tail, null);
tail.next = newNode;
tail = newNode;
}
size++;
}
// 采用头插入法为链表添加新结点
public void addAtHeader(T element) {
header = new Node(element, null, header);
if (tail == null) {
tail = header;
}
size++;
}
// 删除链表中指定索引处的元素
public T delete(int index) {
if (index < 0 || index > size - 1) {
throw new IndexOutOfBoundsException("线性表索
引越界!");
}
Node del = null;
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160if (index == 0) {
del = header;
header = header.next;
header.prev = null;
} else {
// 获取删除点的前一个结点
Node prev = getNodeByIndex(index - 1);
// 获取将要被删除的结点
del = prev.next;
// 让被删除的结点的next指向被删除被删除结点的下一个
结点
prev.next = del.next;
// 让被删除结点的下一个结点的prev指向prev结点
if (del.next != null) {
del.next.prev = prev;
}
// 将被删除结点的prev、next引用赋为null
del.prev = null;
del.next = null;
}
size--;
return del.data;
}
// 删除链表中最后一个元素
public T remove() {
return delete(size - 1);
}
// 判断链表是否为空
public boolean empty() {
return size == 0;
}
// 清空链表
public void clear() {
header = null;
tail = null;
size = 0;
}
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200public String toString() {
if (empty()) {
return "[]";
} else {
StringBuilder sb = new StringBuilder("[");
for (Node current = header; current != null;
current = current.next) {
sb.append(current.data.toString() + ",");
}
int len = sb.length();
return sb.delete(len - 1,
len).append("]").toString();
}
}
// 反向输出链表
public String reverseToString() {
if (empty()) {
return "[]";
} else {
StringBuilder sb = new StringBuilder("[");
for (Node current = tail; current != null;
current = current.prev) {
sb.append(current.data.toString() + ",");
}
int len = sb.length();
return sb.delete(len - 1,
len).append("]").toString();
}
}
//测试代码
public static void main(String[] args) {
DuLinkList<String> list = new DuLinkList<String>
();
list.insert("a", 0);
list.add("b");
list.insert("c", 0);
list.insert("d", 1);
System.out.println(list);
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236另外两种链表,只需要在上述源码基础上稍微改动即可得到,大家自己试
着实现。
3.5 链表数组对比
3.5.1 基础区别
数组简单易用,使用连续的内存空间,大小固定,一经声明就要占用整块
连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间
分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则
可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组
拷贝进去,效率低。
链表没有大小限制,天然支持动态扩容。部分同学可能会有疑问:Java中
的 ArrayList,也可以动态扩容啊?但ArrayList的动态扩容,依赖底层申请一
个更大数组,然后将老的数组数据拷贝过去,此过程中数据拷贝是非常耗
时,效率低,所以两者还是有区别的。
总结:数组随机查询速度快,但增删慢;链表是增删快,但随机查询
慢。
list.delete(2);
System.out.println(list);
System.out.println("反转: " +
list.reverseToString());
System.out.println("c在链表中位置:" +
list.locate("c"));
System.out.println("链表索引处1的元素:" +
list.get(1));
list.remove();
System.out.println("调用remove()方法后的链表:" +
list);
list.delete(0);
System.out.println("调用delete()方法后的链表:" +
list);
}
}
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
2523.5.2 相关面试题
题目1:简述LinkedList 和 ArrayList 的区别。
ArrayList 的实现基于数组,LinkedList 的实现基于双向链表、
对于随机访问,ArrayList 优于 LinkedList,ArrayList 可以根据下标对元素
进行随机访问。而 LinkedList 中元素的查找,只能从链表头或尾逐个遍
历直到找到为止
对于插入和删除操作,LinkedList 优于 ArrayList,因为当元素被添加到
LinkedList任意位置的时候,不需要像 ArrayList 那样整体移动其他元素
LinkedList 比 ArrayList 更占内存,因为 LinkedList 的结点除了存储数据,
还存储了两个引用,一个指向前结点,一个指向后结点。
题目2:请定义一个方法,完成单链表的反转。
初始链表: 1->2->3->4->5->NULL
反转后: 5->4->3->2->1->NULL
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* }
* }
*/
class Solution {
//要求:实现该方法,返回链表头结点
public ListNode reverseList(ListNode head) {
ListNode prev = null; // 前指针结点
ListNode curr = head; // 当前指针结点
// 每次循环,都将当前结点指向它前面的结点,然后当前结点和
前结点后移
while (curr != null) {
ListNode nextTemp = curr.next; // 临时结点,暂存
当前结点的下一
结点,用于后移
curr.next = prev; // 将当前结点指向它前面的结点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
214.栈
栈(Stack)这种数据结构,其有一个典型特点:先进后出,后进先出;简
单归纳为:后进先出,英文表示为:Last In First Out,LIFO。具体可见
下图:
从栈的操作特点上来看,栈是一种操作受限的线性表,其只允许在栈的一
端进行数据的插入和删除,这两种操作分别叫做入栈和出栈。
4.1 自定义栈
栈既可用数组实现,也可用链表实现。用数组实现的栈叫顺序栈,用
链表实现的叫链式栈。
顺序栈源码:
prev = curr; // 前指针后移
curr = nextTemp; // 当前指针后移
}
return prev;
}
}
22
23
24
25
26
27
28
/**
* @ClassName: ArrayStack
* @author shaoyb
* @date 2020年12月14日
* @Description: 使用数组实现栈
*
1
2
3
4
5
6*/
public class ArrayStack {
// 栈大小
private int size;
// 默认栈容量
private int DEFAULT_CAPACITY = 10;
// 栈数据
private Object[] elements;
private int MAX_ARRAY_SIZE = Integer.MAX_VALUE-8;
/**
* 默认构造创建大小为10的栈
*/
public ArrayStack(){
elements = new Object[DEFAULT_CAPACITY];
}
/**
* 通过指定大小创建栈
* @param capacity
*/
public ArrayStack(int capacity){
elements = new Object[capacity];
}
/**
* 入栈
* @param element
* @return
*/
public boolean push(Object element){
try {
checkCapacity(size + 1);
elements[size++] = element;
return true;
}catch (RuntimeException e){
return false;
}
}
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47/**
* 检查栈容量是否还够
*/
private void checkCapacity(int minCapacity) {
if(elements.length - minCapacity < 0 ){
throw new RuntimeException("栈容量不够!");
}
}
/**
* 出栈
* @return
*/
public Object pop(){
if(size <= 0){
return null;//栈为空则直接返回null
}
Object obj = elements[size-1];
elements[--size] = null;
return obj;
}
/**
* 获取栈的大小
* @return
*/
public int size(){
return size;
}
//顺序栈测试
public static void main(String[] args) {
ArrayStack stack = new ArrayStack();
for (int i = 1; i < 13; i++) {
boolean push = stack.push(i);
System.out.println("第" + (i) + "次存储数据为:
"+ i +" ,存储结果是: " + push);
}
// stack.push(1);
for (int i=0; i<11; i++) {
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87链式栈源码:
Object pop = stack.pop();
System.out.println(pop);
}
}
}
88
89
90
91
92
/**
* @ClassName: StackBasedOnLinkedList2
* @author shaoyb
* @date 2020年12月14日
* @Description: 单链表实现栈结构【泛型】
*/
public class StackBasedOnLinkedList2<T> {
//定义结点类
private static class Node<T> {
// 结点数据
private T data;
// next指针
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() {
return data;
}
}
// 栈顶结点
private Node<T> top;
public StackBasedOnLinkedList2() {
this.top = null;
}
/**
* 入栈
* @param data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34* @return
*/
public boolean push(T data) {
Node<T> newNode = new Node<>(data,top);
top = newNode;
return true;
}
/**
* 出栈
* @return
*/
public T pop(){
if(top == null){
return null;
}
//记录栈顶结点
Node<T> topNode = top;
top = topNode.next;
topNode.next = null;
return topNode.data;
}
public static void main(String[] args) {
StackBasedOnLinkedList2<Integer> stack = new
StackBasedOnLinkedList2<>();
for(int i = 0; i < 6; i++) {
stack.push(i);
System.out.println("第" + (i+1) + "次入栈,入栈的
值为:" + i);
}
for(int i = 0; i < 8; i++) {
Object pop = stack.pop();
System.out.println("取出的结果: " + pop);
}
}
}
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73双链表实现栈:
/**
* @ClassName: LinkedListStack
* @author shaoyb
* @date 2020年12月14日
* @Description: 基于双向链表的链式栈实现
*/
public class LinkedListStack {
// 栈大小
private int size;
// 存储链表尾结点
private Node tail;
public LinkedListStack(){
this.tail = null;
}
/**
* 入栈
* @param data
* @return
*/
public boolean push(Object data){
Node newNode = new Node(tail,data,null);
if(size > 0){
tail.next = newNode;
}
tail = newNode;
size++;
return true;
}
/**
* 出栈
* @return
*/
public Object pop(){
if((size-1) < 0){
//栈为空
return null;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
394.2 栈的应用
Java里面提供的栈为Stack类,测试源码代码如下:
}
Object data = tail.getData();
tail = tail.prev;
if(tail != null){
tail.next = null;
}
size--;
return data;
}
}
//结点类定义
class Node {
//前驱结点
public Node prev;
//结点数据
private Object data;
//后继结点
public Node next;
public Node(Node prev,Object data,Node next) {
this.prev = prev;
this.data = data;
this.next = next;
}
public Object getData() {
return data;
}
}
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* @ClassName: JdkStack
* @author shaoyb
* @date 2020年12月14日
* @Description: Java提供Stack类测试
*/
1
2
3
4
5
6Stack源码如下:
Vector类源码如下:
public class JdkStack {
public static void main(String[] args) {
//创建栈对象
Stack<String> stack = new Stack<>();
//数据入栈
stack.push("it");
stack.push("test");
//数据出栈
String s = stack.pop();
System.out.println(s);
s = stack.pop();
System.out.println(s);
}
}
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package java.util;
public class Stack<E> extends Vector<E> {
// 无参表达式
public Stack() {
}
//往栈中插入元素
public E push(E item) {
addElement(item);
return item;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package java.util;
public class Vector<E>
1
2
3extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable,
java.io.Serializable
{
protected Object[] elementData; //元素存放数组
protected int elementCount; //栈元素个数
protected int capacityIncrement;//栈容量
// 构造器源码
public Vector() {
this(10);
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int
capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal
Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
// 往栈里面添加元素
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
// 栈容量判断,自动扩容
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40出栈源码实现:
}
// 扩容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity +
((capacityIncrement > 0) ?
capacityIncrement
: oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData,
newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
...
}
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//出栈功能实现
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
1
2
3
4
5
6
7
8
9
10
115.队列
队列(Queue)是一种特殊的线性表,特殊之处在于它只允许在表的前端
(front)进行删除操作,而在表的后端(rear)进行插入操作,核心理
念是First In First Out 先进先出,即 FIFO。可参考下图:
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
// 核心功能实现:注意,下面2个方法都在 Vector父类中定义
public synchronized E elementAt(int index) {
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index
+ " >= " + elementCount);
}
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
315.1 队列概念
队列可借助数组和链表来实现,其基本属性与操作概念如下:
队头 front
删除数据的一端
队尾 rear
插入数据的一端
入队 enQueue
在队尾rear插入元素
出队 deQueue
在队头front删除元素
5.2 常见队列
队列的种类很多,可分为顺序队列,链式队列,循环队列,阻塞队列,
并发队列等。下面我们重点介绍几种常见的队列。
5.2.1 普通队列
底层通过数组实现
初始情况下, front 和 rear 都指向数组的第一个元素
入队时,往 rear 位置插入元素,但注意保证数组索引不越界
出队时,只要队列不空,就从 front 位置取元素,然后 front 往后
偏移一个位置
弊端:每个空间域只能利用一次,空间极度浪费,且非常容易越界!代码实现:
import java.util.Arrays;
/**
* @ClassName: ArrayQueue
* @author shaoyb
* @date 2020年12月14日
* @Description: 使用数组实现 队列
*/
public class ArrayQueue {
// 定义队列结构
// 使用数组来存储我们队列的元素
private Object[] elements;
// 定义队列的大小
private int size;
// 定义队列的初始容量
private int DEFAULT_SIZE = 10;
// 定义一个容量最大
private int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 定义 队列头索引
private int front;
// 定义 队尾索引
private int rear;
/**
* 定义一个默认初始化长度为10 队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26*/
public ArrayQueue() {
elements = new Object[DEFAULT_SIZE];
// 初始化首尾索引
this.front = 0;
this.rear = 0;
}
/**
* 用户可以通过传入的容量来构建队列
* @param capcity
*/
public ArrayQueue(int capcity) {
if(capcity <= 0) {
throw new RuntimeException("队列的初始容量有问
题!");
}
elements = new Object[capcity];
// 初始化首尾索引
this.front = 0;
this.rear = 0;
}
// 定义结构上操作(算法)
// 入队列【支持动态扩容】
public boolean enqueue(Object element) {
// 校验队列容量是否够用,进行扩容! 队尾索引是否会越界
ensureSizeHelper();
// 入队,往队列尾部索引位置,放入元素值,然后 rear往后
偏移
elements[rear++] = element;
size++;
return true;
}
// 出队列
public Object dequeue() {
// 判断队列是否为空
if(front == rear){
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65return null;
}
Object object= elements[front++];
size--;
return object;
}
// 动态扩容机制
// 1、判断队列容量是否够用
private void ensureSizeHelper() {
// 尾索引已经越过队列尾部
if(rear == elements.length){
if(size == elements.length) {
// 容量已满,需要扩容
grow(size);
}else {
// 队尾 越界,需要元素前移
System.out.println("in copy");
// 前面数组空间 有空余,不需要扩容
// 元素整体前移,从0开始放置
for(int i = front; i < rear; i++) {
elements[i-front] = elements[i];
}
rear = rear - front;
front = 0;
System.out.println("front: " + front + ",
rear: " + rear);
}
}
}
// 2、扩容方法
private void grow(int oldSize) {
int newSize = oldSize + (oldSize >> 1); // 扩容
1.5倍
if(newSize - oldSize < 0) {
newSize = DEFAULT_SIZE;
}
if (newSize - MAX_ARRAY_SIZE > 0) {
newSize = capcityFinal(newSize);
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
1045.2.2 循环队列
初始化:数组的 front 和 rear 都指向0
入队:队不满,先队尾位置传值,再 rear=(rear + 1) % maxsize;
出队:队不空,先取队头位置元素, front=(front + 1)%maxsize;
是否为空: return rear == front;
大小: return (rear+maxsize-front)%maxsize;
注意事项:数组长度 len 的循环队列最多放 len-1 个元素
}
// 完成扩容
elements = Arrays.copyOf(elements, newSize);
}
private int capcityFinal(int newSize) {
return (newSize > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE - 8 : newSize;
}
@Override
public String toString() {
return Arrays.toString(elements);
}
}
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/**
* @ClassName: CircularQueue
* @author shaoyb
* @date 2020年12月14日
* @Description: 使用数组实现 循环队列
1
2
3
4
5*/
public class CircularQueue {
//存储队列数据的数组
private Object[] elements;
//默认数组容量
private int DEFAULT_CAPACITY=10;
//队列中元素个数
private int size;
// 队列头指针
private int front;
//队列尾指针
private int rear;
/**
* 默认构造函数
*/
public CircularQueue() {
elements = new Object[DEFAULT_CAPACITY];
front = 0;
rear = 0;
}
/**
* 通过传入的容量参数构造队列
* @param capacity
*/
public CircularQueue(int capacity) {
elements = new Object[capacity];
front = 0;
rear = 0;
}
// 队列判断是否已满,注意:数组长度 len 的队列最多放 len-1
个元素
public boolean isFull() {
if(front == (rear + 1) % elements.length) {
//队列已满
return true;
}
return false;
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45}
/**
* 元素入队列
* @param element
* @return
*/
public boolean enqueue(Object element){
//判断队列是否已满
if(isFull()) {
return false;
}
//将元素存入rear位置上
elements[rear] = element;
//尾指针后移
/*rear++;
if(rear == elements.length){
rear = 0;
}*/
rear = (rear + 1) % elements.length;
size++;
return true;
}
// 队列判空
public boolean isEmpty() {
if(front == rear){
return true;
}
return false;
}
/**
* 元素出队列
* @return
*/
public Object dequeue(){
//判断队列是否为空
if(isEmpty()) {
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
865.2.3 链式队列
初始化:队列结点 front 和 rear 都设置为 null
入队:新建结点,让 rear 指向新结点
出队:取出 front 位置元素值,让 front 指向后面结点
是否为空:判断 front 和 rear 是否为null
大小:专门定义 size 属性记录
注意事项:单链表实现 自动扩容
return null;
}
//获取front位置上的元素
Object element = elements[front];
//头指针后移
/*front++;
if(front == elements.length){
front = 0;
}*/
front = (front+1)%elements.length;
size--;
return element;
}
/**
* 获取队列大小
* @return
*/
public int getSize() {
return size;
}
}
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110/**
* @ClassName: LinkedListQueue
* @author shaoyb
* @date 2020年12月14日
* @Description: 使用单链表 实现 链式队列
* 入队列(链表的尾部添加一个元素)
* 出队列(链表的头结点删除)
*/
public class LinkedListQueue {
// 队列中元素个数
private int size;
private Node front;
private Node rear;
// 无参构造器
public LinkedListQueue(){
this.front = null;
this.rear = null;
size = 0;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20}
// 入队列
public boolean enqueue(Object element){
// 创建新结点
Node node = new Node(element,null);
// 判断队列是否为空
if(rear == null){
rear = node;
front = node;
}else {
// 让原队尾 next域指向 新结点
rear.next= node;
// 修改队尾 引用值 也指向 新结点
rear = node;
}
size++;
return true;
}
// 出队列
public Object dequeue(){
// 队空 判断
if(front == null) {
return null;
}
Object data = front.data;
front = front.next;
// 出队列后 队列成空
if(front == null){
rear = null;
}
size--;
return data;
}
/**
* 链表的结点定义完毕
*/
static class Node {
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
616.树
树是一种非线性的数据结构,它由n(n>=1)个有限结点组成一个具有层
次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,它是根
朝上,而叶朝下的。具体可见下图:
6.1 树的特点
每个结点有零个或多个子结点;
没有父结点的结点称为根结点;
每一个非根结点有且只有一个父结点;
除了根结点外,每个子结点可以分为多个不相交的子树;
树里面没有环路。
具体可参考上面图形。下面为不是树的情况:
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
}
}
62
63
64
65
66
67
68
69
706.2 结点概念
结点有根结点、父结点、子结点、兄弟结点、叶子结点,具体可见下
图:
结点相关概念: 高度(Heigh) ,深度(Depth),层(Level):
结点高度:结点到叶子结点的最长路径(边数),所有叶子结点的高度为
0
结点深度:根结点到这个结点所经历的边的个数,根的深度为 0
结点层数:结点的深度+1
树的高度:根结点的高度6.3 二叉树
二叉树,顾名思义,其每个结点最多只能有两个子结点,且有左右之
分。每个根结点左边的“叉”称为“左子树”,右边的“叉”称为“右子树”。
下面几种都是二叉树:
满二叉树
叶子结点全在最底层,除了叶子结点外,每个结点都有左右两个子结点
完全二叉树
叶子结点在最底下两层,最后一层叶子结点都靠左排列,并且除了最后
一层,其他层的结点个数都要达到最大
6.4 二叉树存储
想要存储二叉树,有两种表示方式:数组存储、链表存储
6.4.1 链表存储6.4.2 数组存储
据上图可知,完全二叉树能最大限度的利用存储空间。下图为非完全二
叉树存储效果:6.5 二叉排序树
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search
Tree),亦称二叉搜索树,是二叉树中比较常用的一种类型。
排序二叉树特点:
若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
左、右子树也分别为二叉排序树;
没有键值相等的结点。
现有序列:6 2 9 1 7 3 8 5,使用二叉排序树存储,如下图:6.5.1 树的定义
定义排序二叉树类,内部只需要包含一个根结点即可。
结点类包含2个指针域和1个数据域,指针域分别指向左右两个子结点。
/**
*
* @ClassName: BinarySortTree
* @author shaoyb
* @date 2020年12月20日
* @Description: 排序二叉树、二叉搜索树
* 链式存储
*/
public class BinarySortTree {
// 根结点
private Node root;
/*
* 构建结点,内部类
*/
private static class Node {
// 定义数据域
private int value;
// 左子结点引用
private Node left;
// 右子结点引用
private Node right;
// 构造方法
protected Node(Node left, int value, Node right) {
this.left = left;
this.value = value;
this.right = right;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
326.5.2 插入操作
往二叉树中插入元素值,应该先比较插入值与根结点值,插入值小于根结
点值则往左子树找到合适位置插入,大则往右子树找到合适位置插入。
public class BinarySortTree {
//...
// 往二叉树中插入新结点
public void add(int value) {
Node node = new Node(null, value, null);
if(root == null)
root = node;
else
root.add(node);
}
private static class Node {
// ...
// 结点添加【把当前结点看成一棵树的根结点,将新结点添加到
此树中】
public void add(Node node) {
// 递归结束条件
if(node == null)
return;
// 比较插入结点值 与 根结点值
if(node.value < this.value) {
// 结点值 小于 根结点,左子树找到合适位置插入
if(this.left == null) {
this.left = node;
}else {
this.left.add(node);
}
}else if(node.value > this.value) {
// 结点值 大于 根结点,右子树找到合适位置插入
if(this.right == null) {
this.right = node;
}else {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
366.5.3 查找操作
查找操作类似插入操作,先比较查找值与根结点值,相等直接返回根结
点,小于继续往左子树查,大于继续往右子树查。
this.right.add(node);
}
}else {
// 两个结点值相同,不能插入
return;
}
}
// ...
}
}
37
38
39
40
41
42
43
44
45
46
47
public class BinarySortTree {
// ...
// 查找指定结点
public Node search(int value) {
if(root == null)
return null;
return root.search(value);
}
private static class Node {
// ...
// 查找指定结点
public Node search(int value) {
if(value == this.value) {
return this;
}else if(value < this.value) {
if(this.left == null)
return null;
return this.left.search(value);
}else {
if(this.right == null)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
256.5.4 遍历操作
二叉树的遍历分为三种:前序遍历、中序遍历、后序遍历。
前序遍历
从根结点开始,对于树中任意结点,先遍历该结点,再遍历其左子树,
最后遍历其右子树。
中序遍历
从根结点开始,对于树中任意结点,先遍历该结点左子树,再遍历当前
结点,最后遍历右子树。
后序遍历
从根结点开始,对于树中任意结点,先遍历该结点左子树,再遍历右子
树,最后遍历当前结点。
开发中我们推荐中序遍历,因为可以自动排序,具体可参考下图:
return null;
return this.right.search(value);
}
}
// ...
}
}
26
27
28
29
30
31
32
33
34中序遍历源码:
public class BinarySortTree {
// ...
// 中序遍历
public void ldr() {
if(root == null) {
System.out.println("二叉树为空");
return;
}
root.ldr(root);
System.out.println();
}
// ...
private static class Node {
// ...
// 中序遍历
public void ldr(Node node) {
if(node == null)
return;
// 先遍历 左子树
ldr(node.left);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
276.5.5 查找父结点
如果需要删除指定元素结点,则必须找到该元素结点的父结点。
// 再输出当前根结点
System.out.print(" "+ node.value + " ");
// 再遍历 右子树
ldr(node.right);
}
//...
}
}
28
29
30
31
32
33
34
35
36
public class BinarySortTree {
// ...
// 从根元素开始查找元素父结点
public Node searchParent(int value) {
if(root == null)
return null;
return root.searchParent(value);
}
// ...
private static class Node {
// ...
// 找指定元素父结点
public Node searchParent(int value) {
if((this.left != null && this.left.value ==
value)
|| (this.right != null &&
this.right.value == value)) {
return this;
}else {
// 需要查找的元素值 比当前结点值小,则往左子树继
续查找
if(this.value > value && this.left !=
null) {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
246.5.6 删除操作
删除操作较复杂,可分成3种情况,具体见下图:
具体删除操作如下:
return this.left.searchParent(value);
}else if(this.value < value && this.right
!= null) {
return this.right.searchParent(value);
}
}
return null;
}
//...
}
}
25
26
27
28
29
30
31
32
33
34
35
36public class BinarySortTree {
// ...
// 获取树中最小元素值,并删除该结点
public int minDelete(Node r) {
Node target = r;
// 获取到整棵树 最左边的叶子结点【最小值结点】
while(target.left != null) {
target = target.left;
}
int minValue = target.value;
// 删除该结点
delete(minValue);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 返回最小值
return minValue;
}
// 删除指定结点【从根结点开始 查找整个树,完成删除操作】
public void delete(int value) {
if(root == null)
return;
// 获取需要删除的结点
Node target = this.search(value);
if(target == null)
return;
// 需要删除的结点为根结点,其父结点为null
// 5 5 5 5
// 2 8 2 8
if(root.value == target.value) {
if(root.left == null && root.right == null) {
// 1.整个树只有 根结点
root = null;
return;
}else if(root.left != null && root.right ==
null) {
// 2.根结点只存在左子树
root = root.left;
return;
}else if(root.right != null && root.left ==
null) {
// 3.根结点只存在右子树
root = root.right;
return;
}else {
// 4.根结点存在左右子树:按照下面流程操作即可
}
}
// 获取删除结点的父结点
Node parentNode = this.searchParent(value);
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54至此,排序二叉树的基本功能实现完毕!
// 删除情况分 3 种
if(target.left == null && target.right == null) {
// 1.当前结点为叶子结点,直接删除
if(parentNode.left.value == value) {
parentNode.left = null;
}else {
parentNode.right = null;
}
}else if(target.left != null && target.right !=
null) {
// 2.当前结点存在左右子树,找出右子树最小值删除
int min = minDelete(target.right);
target.value = min;
}else {
// 3.当前结点只有一个子树 【将父结点 连接 当前结点的
子结点】
if(target.left == null) {
// 删除结点 右子树存在
if(parentNode.left.value == value) {
parentNode.left = target.right;
}else {
parentNode.right = target.right;
}
}else {
// 删除结点 左子树存在
if(parentNode.left.value == value) {
parentNode.left = target.left;
}else {
parentNode.right = target.left;
}
}
}
}
// ...
}
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89二、排序算法
1.冒泡排序
冒泡(Bubble Sort)排序是一种简单排序算法,它通过依次比较交换两个
相邻元素实现功能。每一次冒泡会让至少一个元素移动到它应该在的位
置上,这样 n 次冒泡就完成了 n 个数据的排序工作。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶
端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮
到顶端一样,故名“冒泡排序”。
冒泡排序算法实现步骤:
1. 比较相邻的元素,如果第一个比第二个大,就交换他们两个。
2. 对每一对相邻元素重复上述工作,从第一对到最后一对。完成后,最
大的数会放到最后位置。
3. 针对所有的元素重复以上的步骤,除了最后一个。
4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字
需要比较。
冒泡排序过程具体见下图:源码实现:
import java.util.Arrays;
import org.junit.Test;
/**
* @ClassName: BubbleSort
* @author shaoyb
* @date 2020年12月10日
* @Description: 冒泡排序
* 冒泡排序思路:
* 1. 比较相邻的元素,如果第一个比第二个大,就交换他们两个。
* 2. 对每一对相邻元素重复上述工作,从第一对到最后一对。完成后,
最大的数会放到最后位置。
* 3. 针对所有的元素重复以上的步骤,除了最后一个。
* 4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数
字需要比较。
*/
public class BubbleSort {
//基础冒泡排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public void bubbleSort(int[] array) {
// 待排序元素的个数
int length = array.length;
if(length <= 1) {
return;
}
// 冒泡排序
for(int i = 1; i < length; i++) {
// 相邻比较
for (int j = 0; j < length-i; j++){
if(array[j] > array[j+1]){
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
//System.out.println(Arrays.toString(array));
}
}
@Test
public void testBubbleSort() {
int[] array = new int[]{5,2,6,9,0,3};
System.out.println("排序前 : " +
Arrays.toString(array));
//进行排序
bubbleSort(array);
//输出排序结果
System.out.println("排序后 : " +
Arrays.toString(array));
}
/**
* 冒泡排序优化
* @param array
*/
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56public void bubbleSort2(int[] array){
int len = array.length;
if(len <= 1){
return;
}
//外层循环控制整体排序次数
for(int i = 1; i < len;i++){
//是否需要提前结束冒泡的标识
boolean flag = true;
for(int j = 0; j < len - i; j++){
//判断前后数据是否需要交换 如果前一个数据大于后
一个数据则进行交换否则不交换
if(array[j] > array[j+1]){
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
flag = false;
}
}
//System.out.println(Arrays.toString(array));
//在当前冒泡排序中如果所有元素都未进行交换,则说明排
序已完成,无需进行后续循环操作
if(flag){
break;
}
}
}
@Test
public void testBubbleSort2(){
int[] array = new int[]{5,2,6,9,0,3};
System.out.println("排序前 : " +
Arrays.toString(array));
//进行排序
bubbleSort2(array);
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
942.选择排序
选择排序(Selection Sort)的原理有点类似插入排序,也分已排序区间和
未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将
其放到已排序区间的末尾,最终完成排序。
选择排序算法描述:
1. 初始状态:无序区间为 Arr[0.1..n],有序区间为空;
2. 第i==1趟排序开始,从无序区中选出最小的元素Arr[k],将它与无序区
的第1个元素交换,从而得到有序区间Arr[0..i-1],无序区间Arr[i..n];
3. 继续后面第i趟排序(i=2,3…n-1),重复上面第二步过程;
4. 第n-1趟排序结束,数组排序完成。
选择排序过程如下图:
//输出排序结果
System.out.println("排序后 : " +
Arrays.toString(array));
}
}
95
96
97
98源码实现:
import java.util.Arrays;
import org.junit.Test;
/**
* @ClassName: SelectionSort
* @author shaoyb
* @date 2020年12月10日
* @Description: 选择排序
* 选择排序步骤:
* 1. 初始状态:无序区间为 Arr[0.1..n],有序区间为空;
* 2. 第i==1趟排序开始,从无序区中选出最小的元素Arr[k],将它与无序
区的第1个元素交换,从而得到有序区间Arr[0..i-1],无序区间
Arr[i..n];
* 3. 继续后面第i趟排序(i=2,3…n-1),重复上面第二步过程;
* 4. 第n-1趟排序结束,数组排序完成。
*/
public class SelectionSort {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public void selectionSort(int[] arr) {
int len = arr.length;
if(len <= 1)
return;
//外层循环控制总体排序次数
for(int i = 0; i < len-1; i++) {
int minIndex = i;
//内层循环找到当前无序列表中最小下标
for(int j = i + 1; j < len; j++) {
if(arr[minIndex] > arr[j]) {
minIndex = j;
}
}
//将无需列表中最小值添加到 有序列表最后位置
if(minIndex != i) {
arr[minIndex] = arr[minIndex] ^ arr[i];
arr[i] = arr[minIndex] ^ arr[i];
arr[minIndex] = arr[minIndex] ^ arr[i];
}
//System.out.println(Arrays.toString(arr));
}
}
/**
* 测试插入排序
*/
@Test
public void testSelectionSort() {
//准备一个int数组
int[] array = {5, 2, 6, 5, 9, 0, 3};
System.out.println("排序前: "+
Arrays.toString(array));
//插入排序
selectionSort(array);
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
553.插入排序
插入排序(Insertion Sort),一般也被称为直接插入排序。对于少量元素
的排序,它是一个有效的算法。
插入排序算法描述:
1. 将数组分成两部分,已排序、未排序区间,初始情况下,已排序区间
只有一个元素,即数组第一个元素;
2. 取未排序区间中第一个元素,插入到已排序区间中合适的位置,这样
子就得到了一个更大的已排序区间;
3. 重复这个过程,直到未排序区间中元素为空,算法结束。
插入排序过程见下图:
源码实现:
//输出排序结果
System.out.println("排序后: "+
Arrays.toString(array));
}
}
56
57
58
59
import java.util.Arrays;
import org.junit.Test;
1
2/**
* @ClassName: InsertionSort
* @author shaoyb
* @date 2020年12月10日
* @Description: 插入排序
* 具体操作步骤:
* 1、取第一个元素,作为已排序区间;
* 2、取下一个元素,从后往前扫描已排序区间的所有元素,找到合适位置
插入;
* 3、扫描过程中,如果(已排序区间)元素值大于新元素,将该元素移到
下一位置;
* 4、继续往前扫描,重复步骤3,直到找到(已排序区间)元素小于或者等于
新元素的位置;
* 5、将新元素插入到该位置后;
* 6、重复步骤2~5。
*/
public class InsertionSort {
public void insertionSort(int[] arr) {
int len = arr.length;
if(len <= 1) {
return;
}
//外层循环控制 总体循环次数
for(int i = 1; i < len; i++) {
//内层循环做的事情:将无序列表中第一个元素插入到有序
列表中合适位置
int value = arr[i];
//获取有序列表中最后一个元素下标
int j = i - 1;
for(; j >= 0; j--) {
if(value < arr[j]) {
arr[j+1] = arr[j];
}else {
break;
}
}
//将需要插入的元素 放置到合适位置
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
394.希尔排序
希尔(shell)排序是Donald Shell于1959年提出的一种排序算法。希尔排序
也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版
本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之
一。
希尔排序对直接插入排序改进的着眼点:
若待排序序列中 元素基本有序 时,直接插入排序的效率可以大大提高
如果待排序序列中 元素数量较小 时,直接插入排序效率很高
arr[j+1] = value;
//一次排序完成后,输出 方便 观察
System.out.println(Arrays.toString(arr));
}
}
/**
* 测试插入排序
*/
@Test
public void testInsertionSort() {
//准备一个int数组
int[] array = {5, 2, 6, 5, 9, 0, 3};
System.out.println("排序前: "+
Arrays.toString(array));
//插入排序
insertionSort(array);
//输出排序结果
System.out.println("排序后: "+
Arrays.toString(array));
}
}
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63希尔排序算法思路:
将整个待排序序列分割成若干个子序列,在子序列内部分别进行直接插
入排序,等到整个序列 基本有序 时,再对全体成员进行直接插入排序!
待解决问题:
如何分割子序列,才能保证最终能得到基本有序?
子序列内部如何进行直接插入排序?
分割方案
1. 将有n个元素的数组分成n/2个数字序列,第i个元素和第i+n/2,
i+n/2*m...个元素为一组;
2. 对一组数列进行简单插入排序;
3. 然后,调整增量为n/4,从而得到新的几组数列,再次排序;
4. 不断重复上述过程,直到增量为1,shell排序完全转化成简单插入排
序,完成该趟排序,则数组排序成功。
希尔排序流程:具体源码实现:
import java.util.Arrays;
import org.junit.Test;
/**
* @ClassName: ShellSort
* @author shaoyb
* @date 2020年12月10日
* @Description: 希尔排序
* 希尔排序步骤:
* 1. 将有n个元素的数组分成n/2个数字序列,第i个元素和第i+n/2,
i+n/2*m...个元素为一组;
* 2. 对一组数列进行简单插入排序;
* 3. 然后,调整增量为n/4,从而得到新的几组数列,再次排序;
1
2
3
4
5
6
7
8
9
10
11
12* 4. 不断重复上述过程,直到增量为1,shell排序完全转化成简单插入排
序,完成该趟排序,则数组排序成功。
*/
public class ShellSort {
//由简单插入排序 改造得到 shell排序
public void shellSort(int[] array) {
int len = array.length;
if(len <= 1)
return;
//设置初始增量
int gap = len / 2;
//由增量控制整体排序次数
while(gap > 0) {
//插入排序改造
for(int i = gap; i < len; i++) {
//记录要插入的值
int value = array[i];
//有序序列的最后一个元素下标
int j = i - gap;
for(; j >= 0; j -= gap) {
if(value < array[j]) {
array[j + gap] = array[j];
}else {
break;
}
}
array[j+gap] = value;
}
System.out.println(Arrays.toString(array));
gap = gap / 2;
}
}
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
525.归并排序
归并排序(Merge Sort)算法,使用的是分治思想。分治,顾名思义,就
是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决
了,大问题也就解决了。
核心源码: mergeSort(m->n) = merge(mergeSort(m->k),mergeSort(k+1-
>n));
算法思路:
如果要排序一个数组,先把数组从中间分成前后两部分,然后对前后两部
分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序
了。具体见下图:
/**
* shell排序测试
*/
@Test
public void testSelectionSort() {
//准备一个int数组
int[] array = {5, 2, 6, 5, 9, 0, 3};
System.out.println("排序前: "+
Arrays.toString(array));
//shell排序
shellSort(array);
//输出排序结果
System.out.println("排序后: "+
Arrays.toString(array));
}
}
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69注意:分治思想跟递归思想很相似。分治是一种解决问题的处理思想,
递归是一种编程技巧,这两者并不冲突,分治算法一般都是用递归来实
现。
具体代码实现如下:
import java.util.Arrays;
import org.junit.Test;
/**
*
* @ClassName: MergeSort
* @author shaoyb
* @date 2020年12月9日
* @Description: 归并排序
* 归并排序思路:
* 1、把长度为n的序列一分为二成两个子序列;
2、对这两个子序列分别采用归并排序;
3、将两个排序好的子序列合并成一个最终的排序序列。
1
2
3
4
5
6
7
8
9
10
11
12
13*/
public class MergeSort {
/**
* 归并排序算法实现
* @param arr 需要排序的数组
* @return 排序成功后新数组
*/
public int[] mergeSort(int[] arr){
//1.确定递归终止条件
if(arr.length < 2) {
return arr;
}
//2.拆解数组成左右两部分
int mid = arr.length/2;
int[] left = Arrays.copyOfRange(arr,0,mid);
int[] right =
Arrays.copyOfRange(arr,mid,arr.length);
//3.对拆解后两个数组进行合并
return merge(mergeSort(left),mergeSort(right));
}
/**
* 合并两个有序数组,并返回合并后的新数组
* @param left
* @param right
*/
public int[] merge(int[] left,int[] right) {
//1.定义好新数组
int[] newArray = new int[left.length +
right.length];
//2.往新数组中逐个添加元素
int lIndex = 0;
int rIndex = 0;
for(int i = 0; i < newArray.length; i++) {
if(lIndex >= left.length) {
//左数组已经遍历完成
newArray[i] = right[rIndex++];
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
526.快速排序
快速排序(Quick Sort)算法,简称快排,利用的是分治的思想。
快速排序思路:
}else if(rIndex >= right.length) {
//右数组已经遍历完成
newArray[i] = left[lIndex++];
}else if(left[lIndex] < right[rIndex]) {
//左数组当前元素值小于右数组
newArray[i] = left[lIndex++];
}else {
//右数组当前元素值小于左数组
newArray[i] = right[rIndex++];
}
}
return newArray;
}
@Test
public void testMergeSort(){
//1.定义数组
int[] array = new int[] {5,2,6,9,0,3};
System.out.println("排序前" +
Arrays.toString(array));
//2.归并排序
array = mergeSort(array);
System.out.println("排序后" +
Arrays.toString(array));
}
}
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80如果要对 first->end 之间的数列进行排序,我们选择 first->end 之间的任
意一个元素数据作为分区点(轴值Pivot),然后遍历 first->end 之间所有元
素,将小于pivot 的元素放到左边,大于pivot的元素放到右边,pivot放到中
间,这样整个数列就被分成了三部分。first->i-1 之间的元素是小于 pivot
的,中间是 pivot,i+1->end 之间的元素是大于 pivot 的。然后再根据分治递
归的思想处理两边区间的元素数列,直到区间缩小为 1,整个数列就有序
了。
具体可见下图:
排序算法核心:
1. 确定轴值,一般取序列中第一位置值
2. 找到序列中轴值所在位置,放置轴值
3. 将小于轴值的所有元素往轴值前面放置,大于轴值的元素往轴值后面
放置
具体代码实现如下:
import java.util.Arrays;
import org.junit.Test;
1
2
3/**
*
* @ClassName: QuickSort
* @author shaoyb
* @date 2020年12月8日
* @Description: 快速排序
* 快速排序算法思想
* 1、将序列中第一个元素,设置为“轴值”
2、对序列排序,所有比轴值小的元素摆放在轴值前面,比轴值大的摆
在轴值后面(相同的数可以到任一边)。这样子操作完成后,该轴值就处于
数列的中间位置,以上过程称为分区(partition)操作。
3、对轴值前的子序列和轴值后的子序列进行递归快速排序。
*/
import java.util.Arrays;
public class QuickSort {
//获取轴值的位置,并轴值为基准点调整序列位置
public int partition(int arr[],int first,int end)
{
int i = first;
int j = end;
//默认轴值为 arr[i];
while(i<j){
//让j从后往前移动
while(i<j && arr[i] <= arr[j])
j--;
if(i < j){
//交换i j位置的值
swap(arr,i,j);
//更新i的值
i++;
}
//让i从前往后移动
while(i < j && arr[i] <= arr[j])
i++;
if(i < j){
//再次交换i j位置的值
swap(arr,i,j);
j--;
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42}
}
return i;
}
//快速排序
public void quickSort(int arr[],int start,int end)
{
if(start >= end)
return;
//先分段
int index = partition(arr,start,end);
//对前半截快速排序
quickSort(arr,start,index-1);
//对后半截快速排序
quickSort(arr,index+1,end);
}
//交换数组中两个元素
private void swap(int[] arr, int i, int j) {
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
public static void main(String[] args) {
int[] array = {3,7,9,1,2,6};
System.out.println("排序前: "+
Arrays.toString(array));
TestSort t = new TestSort();
t.quickSort(array, 0, array.length-1);
System.out.println("排序后: "+
Arrays.toString(array));
}
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
817.桶排序
桶排序(Bucket Sort),顾名思义,会用到“桶”,我们可以认为"桶"就是
一个容器。核心思想是将要排序的数据分到几个桶里,然后对每个桶里
的数据进行排序,最后遍历所有桶中数据即可。
算法实现思路:
1. 设置固定空桶数
2. 将数据放到对应的空桶中
3. 将每个不为空的桶进行排序
4. 拼接不为空的桶中的数据,得到结果
如下图所示:
}
82关键点1:元素值域的划分,即桶中元素的个数,这也决定了桶的数量
关键点2:如何将数组中元素映射到各个桶中
关键点3:如何对各个桶中元素进行排序
注意:待排序数列分布要均匀,避免空桶;同时要兼顾效率和空间,避
免空间浪费。
算法实现:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
/**
*
* @ClassName: BucketSort
* @author yonng
* @date 2020年12月9日
* @Description:
* 桶排序思路:
* 1. 设置固定空桶数
* 2. 将数据放到对应的空桶中
* 3. 将每个不为空的桶进行排序
* 4. 拼接不为空的桶中的数据,得到结果
*/
public class BucketSort {
/**
* 桶排序
*
* @param array 待排序集合
* @param bucketSize 桶中可放入元素个数,即每个桶所能放置
多少个不同数值
* 例如当BucketSize==5,该桶可放{1,2,3,4,5}这
几种数字,但容量不限,即可存放100个3)
* @return 排好序后的集合
*/
public List<Integer> bucketSort(List<Integer> array,
int bucketSize) {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28//1.递归算法终止条件,合法性校验
if (array == null || array.size() < 2 ||
bucketSize < 1) {
return array;
}
//2.确定桶的个数
// a.找出我们集合中的元素的最大值和最小值
int max = array.get(0);
int min = array.get(0);
for(int i = 0; i < array.size(); i++) {
if (array.get(i) > max) {
max = array.get(i);
}
if (array.get(i) < min) {
min = array.get(i);
}
}
// b.计算桶的个数
int bucketCount = (max - min) / bucketSize + 1;
//3.将数据分别放到对应的空桶(list)中
// a.创建所有桶
List<List<Integer>> bucketList = new ArrayList<>
();
for (int i = 0; i < bucketCount; i++) {
bucketList.add(new ArrayList<Integer>());
}
// b.将待排序的数据添加到对应的桶中
for (int i = 0; i < array.size(); i++) {
//找到该数据应该放置的桶
int bucketIndex = (array.get(i) - min) /
bucketSize;
//将该数据 放入对应桶中
bucketList.get(bucketIndex).add(array.get(i));
}
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65//4.桶内元素的排序
List<Integer> reustList = new ArrayList<>();
// a.遍历所有桶,逐个进行排序
for (int i = 0; i < bucketList.size(); i++) {
List<Integer> everyBucket =
bucketList.get(i);
/* 此处为核心代码,不容易理解,可画图分析 */
if(everyBucket.size() > 0) {
//桶个数为1,则桶容量减1
if(bucketCount == 1) {
bucketSize--;
}
// b.对子桶进行排序(递归实现 桶排序)
List<Integer> temp =
bucketSort(everyBucket, bucketSize);
// c.将排好序的
for (int j = 0; j < temp.size(); j++) {
reustList.add(temp.get(j));
}
}
}
return reustList;
}
/**
* 桶排序测试
*/
@Test
public void testBucketSort() {
Integer[] array = {5,2,2,6,9,0,3,4};
List<Integer> list = Arrays.asList(array);
System.out.println("排序前: " + list);
List<Integer> bucketSort = bucketSort(list, 3);
System.out.println("排序后: " + bucketSort);
}
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104桶排序总结:桶排序比较适合用在外部排序中,所谓外部排序,就是说
数据存储在外部磁盘中,数据量比较大,而内存有限,无法将数据全部
加载到内存中直接进行排序。具体可见参考面试题:
比如说我们有 10GB 的订单数据,我们希望按订单金额(假设金额都是正
整数)进行排序,但是我们的内存有限,只有几百 M,没办法一次性把
10GB 的数据都加载到内存中进行排序。此时该怎么办呢?
我们可以借助桶排序的处理思想来解决这个问题,具体思路如下:
先扫描一遍文件,看订单金额所处的数据范围。假设经过扫描之后我们得
到,订单金额最小是 1 元,最大是 10 万元。我们将所有订单根据金额划分
到 100 个桶里,第一个桶我们存储金额在 1 元到 1000 元之内的订单,第二
桶存储金额在 1001 元到 2000 元之内的订单,以此类推。每一个桶对应一
个文件,并且按照金额范围的大小顺序编号命名(00,01,02…99)。
理想的情况下,如果订单金额在 1 到 10 万之间均匀分布,那订单会被均
匀划分到 100 个文件中,每个小文件中存储大约 100MB 的订单数据,我们
就可以将这 100 个小文件依次放到内存中,用快速排序来排序。等所有文
件都排好序之后,我们只需要按照文件编号,从小到大依次读取每个小文
件中的订单数据,并将其写入到一个文件中,那这个文件中存储的就是按
照金额从小到大排序的订单数据了。
不过,你可能会发现,订单按照金额在 1 元到 10 万元之间并不一定是均
匀分布的 ,所以 10GB 订单数据是无法均匀地被划分到 100 个文件中的。有
可能某个金额区间的数据特别多,划分之后对应的文件就会很大,没法一
次性读入内存。这又该怎么办呢?
针对这些划分之后还是比较大的文件,我们可以继续划分,比如,订单金
额在 1 元到 1000 元之间的比较多,我们就将这个区间继续划分为 10 个小
区间,1 元到 100 元,101 元到 200 元,201 元到 300 元…901 元到 1000
元。如果划分之后,101 元到 200 元之间的订单还是太多,无法一次性读
入内存,那就继续再划分,直到所有的文件都能读入内存排序为止。
##
}
105
1万+

被折叠的 条评论
为什么被折叠?



