文章目录
一、JAVA 数据结构概述
1、数据结构概述
数据结构包括线性结构和非线性结构
(1)线性结构
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一**的线性关系;
- 线性结构有两种不同的存储结构,即顺序存储结构(数组)和链式存储结构(链表);
- 顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的;
- 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息;
- 线性结构常见的有:数组、队列、链表和栈。
(2)非线性结构
- 非线性结构包括:二维数组,多维数组,广义表,树结构,图结构
二、常用数据结构
1、数组
- 数组是相同数据类型的元素按一定顺序排列的集合,是一块连续的内存空间。
- Java中,Array就是数组,此外,ArrayList使用了数组Array作为其实现基础,它和一般的Array相比,最大的好处是,我们在添加元素时不必考虑越界,元素超出数组容量时,它会自动扩张保证容量。
- Vector和ArrayList相比,主要差别就在于多了一个线程安全性,但是效率比较低下。如今java.util.concurrent包提供了许多线程安全的集合类(比如 LinkedBlockingQueue),所以不必再使用Vector了。
2、链表
(1)链表介绍
- 链表是一种非连续、非顺序的结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,链表由一系列结点组成。
- 链表是有序的列表,是以节点方式来存储的,每个节点包含data域、next域(指向下一个节点)。链表的各个节点不一定是连续存储,链表分带头节点的链表和没有头结点的链表,根据实际的需求来确定。
- Java中,LinkedList 使用链表作为其基础实现。
- 链表分为单链表、双链表、环形链表
3、队列
(1)队列介绍
- 队列是一个有序列表,可以用数组或是链表来实现;
- 队列遵循先入先出的原则。即先存入队列的数据,要先取出,后存入的要后取出;
- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图,其中maxSize是该队列的最大容量;
因为队列的输出、输入是分别从前后端来处理,因此需要两个变量front及rear分别记录队列前后端的下标,front 会随着数据输出而改变,而rear则是随着数据输入而改变。 - 队列是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,亦即所谓的先进先出(FIFO);
- Java中,LinkedList实现了Deque,可以做为双向队列(自然也可以用作单向队列)。另外PriorityQueue实现了带优先级的队列,亦即队列的每一个元素都有优先级,且元素按照优先级排序。
(2)队列示例
① 创建ArrayQueue类,使用数组模拟队列
public class ArrayQueue {
private int maxSize; //数组的最大容量
private int front; //队列头
private int rear; //队列尾
private int[] arr; //用于存放数据,模拟队列
//创建队列的构造器
public ArrayQueue(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[maxSize];
front = -1; //指向队列头部的前一个位置
rear = -1; //指向队列尾的确定位置
}
//判断队列容量是否满
public boolean isFull() {
return rear == maxSize - 1;
}
//判断队列是否为空
public boolean isEmpty() {
return rear == front;
}
//添加数据到队列
public void addQuque(int n) {
//判断队列是否满
if(isFull() == true) {
System.out.println("队列满,无法添加数据");
}
rear++; //让rear后移
arr[rear] = n;
}
//获取队列的数据
public int getQueue() {
//判断队列是为空
if(isEmpty() == true) {
System.out.println("队列空,无法取数据");
}
front++; //让rear后移
return arr[front];
}
//显示队列的所有数据
public void showQueue() {
//遍历
if(isEmpty() == true) {
System.out.println("队列空,无法取数据");
return;
}
for(int i = 0; i < arr.length; i++) {
System.out.printf("arr[%d] = %d\n", i, arr[i]);
}
}
//显示队列的头数据,这个不是取数据
public int headQueue() {
if(isEmpty() == true) {
throw new RuntimeException("队列空,无法取数据");
}
return arr[front+1];
}
}
② 测试队列
public class Test {
public static void main(String[] args) {
//创建一个队列
ArrayQueue arrayQueue = new ArrayQueue(3);
char key = ' '; //接受用户数据
Scanner sc = new Scanner(System.in);
boolean loop = true;
//输出一个菜单
while(loop) {
System.out.println("s(show):显示数据");
System.out.println("e(exit):退出程序");
System.out.println("a(add):添加数据");
System.out.println("g(get):取出数据");
System.out.println("h(head):查看头数据");
key = sc.next().charAt(0);
switch (key) {
case 's':
arrayQueue.showQueue();
break;
case 'a':
try {
System.out.println("请输入一个数:");
int value = sc.nextInt();
arrayQueue.addQuque(value);
}catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'g':
try {
int data = arrayQueue.getQueue();
System.out.println("取出的数据 = " + data);
}catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'h':
try {
int data = arrayQueue.headQueue();
System.out.println("队列头数据 = " + data);
}catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'e':
sc.close();
loop = false;
break;
default:
break;
}
}
System.out.println("退出");
}
}
4、栈
- 栈(stack)又名堆栈,是一个先入后出的有序列表,最先放入的元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除;
- 栈是一种运算受限的线性表,限制现行表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表,允许插入和删除的一段为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom);
- Java中,Stack实现了这种特性,但是Stack也继承了Vector,所以具有线程安全线和效率低下两个特性,最新的JDK8中,推荐用Deque来实现栈。
5、集合
- 集合是指具有某种特定性质的具体的或抽象的对象汇总成的集体,这些对象称为该集合的元素,其主要特性是元素不可重复。
- 在Java中,HashSet体现了这种数据结构,而HashSet是在MashMap的基础上构建的。LinkedHashSet继承了HashSet,使用HashCode确定在集合中的位置,使用链表的方式确定位置,所以有顺序。TreeSet实现了SortedSet 接口,是排好序的集合(在TreeMap 基础之上构建),因此查找操作比普通的Hashset要快(log(N));插入操作要慢(log(N)),因为要维护有序。
6、散列表
- 散列表也叫哈希表,是根据关键键值(Keyvalue)进行访问的数据结构,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数。
- Java中HashMap实现了散列表,而Hashtable比它多了一个线程安全性,但是由于使用了全局锁导致其性能较低,所以现在一般用ConcurrentHashMap来实现线程安全的HashMap(类似的,以上的数据结构在最新的java.util.concurrent的包中几乎都有对应的高性能的线程安全的类)。TreeMap实现SortMap接口,能够把它保存的记录按照键排序。LinkedHashMap保留了元素插入的顺序。WeakHashMap是一种改进的HashMap,它对key实行“弱引用”,如果一个key不再被外部所引用,那么该key可以被GC回收,而不需要我们手动删除。
7、树
- 树(tree)是包含n(n>0)个节点的有穷集合,其中,每个元素称为节点(node),有一个特定的节点被称为根节点或树根(root),除根节点之外的其余数据元素被分为m(m≥0)个互不相交的结合T1,T2,……Tm-1,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作原树的子树(subtree)。
- 树这种数据结构在计算机世界中有广泛的应用,比如操作系统中用到了红黑树,数据库用到了B+树,编译器中的语法树,内存管理用到了堆(本质上也是树),信息论中的哈夫曼编码等等等等,在Java中TreeSet和TreeMap用到了树来排序(二分查找提高检索速度),不过一般都需要程序员自己去定义一个树的类,并实现相关性质,而没有现成的API。
(1)二叉树
- 二叉树是一种基础而且重要的数据结构,其每个结点至多只有二棵子树,二叉树有左右子树之分,第i层至多有2(i-1)个结点(i从1开始);深度为k的二叉树至多有2(k)-1)个结点,对任何一棵二叉树,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。
- 二叉树的性质:
- 在非空二叉树中,第i层的结点总数不超过2^(i-1), i>=1;
- 深度为h的二叉树最多有2^h-1个结点(h>=1),最少有h个结点;
- 对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1;
- 具有n个结点的完全二叉树的深度为log2(n+1);
- 有N个结点的完全二叉树各结点如果用顺序方式存储,则结点之间有如下关系: 若I为结点编号则 如果I>1,则其父结点的编号为I/2; 如果2I<=N,则其左儿子(即左子树的根结点)的编号为2I;若2I>N,则无左儿子; 如果2I+1<=N,则其右儿子的结点编号为2I+1;若2I+1>N,则无右儿子。
- 给定N个节点,能构成h(N)种不同的二叉树,其中h(N)为卡特兰数的第N项,h(n)=C(2*n, n)/(n+1)。
- 设有I个枝点,I为所有枝点的道路长度总和,J为叶的道路长度总和J=I+2i。
(2)满二叉树、完全二叉树
- 满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点;
- 完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~(h-1)层) 的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树;
- 满二叉树是完全二叉树的一个特例。
(3)二叉查找树
- 二叉查找树,又称为是二叉排序树(Binary Sort Tree)或二叉搜索树。二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
- 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
- 左、右子树也分别为二叉排序树;
- 没有键值相等的节点。
- 二叉查找树的性质:对二叉查找树进行中序遍历,即可得到有序的数列。
- 二叉查找树的时间复杂度:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡二叉树设计的初衷。
(4)平衡二叉树
- 平衡二叉树又被称为AVL树,具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。它的出现就是解决二叉查找树不平衡导致查找效率退化为线性的问题,因为在删除和插入之时会维护树的平衡,使得查找时间保持在O(logn),比二叉查找树更稳定。
8、堆
- 堆是一颗完全二叉树,在这棵树中,所有父节点都满足大于等于其子节点的堆叫大根堆,所有父节点都满足小于等于其子节点的堆叫小根堆。堆虽然是一颗树,但是通常存放在一个数组中,父节点和孩子节点的父子关系通过数组下标来确定。
- 堆的用途:堆排序,优先级队列。此外由于调整代价较小,也适合实时类型的排序与变更。
9、稀疏数组(sparsearray)
(1)稀疏数组介绍
- 当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
- 稀疏数组的处理方法是:
- 记录数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
(2)稀疏数组分析
有一个二维数组,二维数组的某些位置上有不等于0的元素,当我们要对这些数据进行存储的时候,如果直接存储整个二维数组,就会存储很多没有意义的数据,对资源的消耗和速率有一定的影响,尤其是在数据量比较大的时候。然而当我们用稀疏数组来存储时,可以将有数据的位置上的元素进行存储,而没有数据的位置上的元素则不进行保存,这样就不会存储很多没有意义的数据。
(3)二维数组转稀疏数组
- 遍历原始的二维数组,得到其中的有效数据的个数sum;
- 根据sum的值创建一个稀疏数组,如
int[sum+1][3] sparseArr
; - 将二维数组中的有效数据存入到创建的稀疏数组中;
- 将稀疏数组存盘保存。
(4)稀疏数组转二维数组
- 从存盘中取出存储的稀疏数组;
- 首先读取稀疏数组的第一行,根据第一行的数据创建原始的二维数组,如
int[row][col] Arr
; - 然后读取稀疏数组后面的数据,并赋值到二维数组对应位置中即可。
(5)稀疏数组代码示例
①定义一个二维数组,并随机给其中某几个元素赋值
int oldArr[][] = new int[10][10];
oldArr[1][2] = 1;
oldArr[3][4] = 2;
oldArr[5][6] = 3;
② 二维数组转稀疏数组,首先遍历原有的二维数组,得到有效数据的个数(即非0数据的个数)
int sum = 0;
for(int[] row: oldArr){
for(int data: row){
if(data != 0){
sum++;
}
}
}
③ 根据有效数据的个数创建稀疏数组
int sparseArr[][] = new int[sum+1][3];
//给稀疏数组赋值,先给稀疏数组头部赋值
sparseArr[0][0] = 10;
sparseArr[0][1] = 17;
sparseArr[0][2] = sum;
//遍历原二维数组,将非0元素赋值给稀疏数组
int count = 0;
for(int i = 0; i < oldArr.length; i++){
for(int j = 0; j < oldArr[i].length; j++){
if(oldArr[i][j] != 0){
count++;
sparseArr[count][0] = i;
sparseArr[count][1] = j;
sparseArr[count][2] = oldArr[i][j];
}
}
}
④ 将稀疏数组转为二维数组,先读取稀疏数组的第一行,得到要转的二维数组的行和列数,并定义一个新的二维数组,再依次读取稀疏数组的后几行元素,将元素赋值给新二维数组的对应位置。
int a = sparseArr[0][0];
int b = sparseArr[0][1];
int newArr[][] = new int[a][b];
//依次读取并赋值
for(int i = 1; i < sparseArr.length; i++) {
for(int j = 0; j < sparseArr[i].length; j++) {
newArr[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][j];
}
}
//打印新数组
for(int i = 1; i < newArr.length; i++) {
for(int j = 0; j < newArr[i].length; j++) {
System.out.print(newArr[i][j]+" ");
}
System.out.println();
}
10、递归
(1)递归介绍
递归就是方法自己调用自己,每次调用时传入不同的变量。递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。