八大数据结构及实现
一、简述
在编程过程中,经常会遇到一个问题就是,性能瓶颈,很多时候考虑的都是怎么去做横向扩展,但偏偏忽略了最基本的问题就是系统是否真的已经达到了瓶颈?
性能瓶颈通常的表象:
1、资源消耗过多,外部处理系统的性能不足;
2、或者资源消耗不多但程序的响应速度却仍达不到要求。
调优的方式:
1、寻找过度消耗资源的代码
2、 寻找未充分使用资源但程序执行慢的原因和代码。
基础决定高度
就拿汽车来比较,通常不懂变速箱、发动机的原理但也是能开车,同样一个不懂数据结构和算法的人也能编程。很多人觉得基本的数据结构及操作已经在高级语言中封装,都可以直接调用库函数,那么到底有没有必要好好学习数据结构?
数据结构+算法=程序
通常在程序中,遇到一个实际问题,充分利用数据结构,将数据及其之间的关系有效地存储在计算机中,然后选择合适的算法策略,并用程序高效实现,这才是提高程序性能的主要方式。
如何有效地存储数据,不同的数据结构产生什么样的算法复杂性,有没有更好的存储方法提高算法的效率?
如果没有具备这块相应的知识,怎么完成上述的实现?如果脱离了原有的调用,怎么完成程序的高效实现?而所有的应用实现都依赖于基础,基础就是数据结构和算法。了解这块,才能做到无惧任何技术的演变。所有说基础决定高度!
二、基本的概念
数据结构表示数据在计算机中的存储和组织形式,主要描述数据元素之间和位置关系等。
选择适当的数据结构可以提高计算机程序的运行效率(时间复杂度)和存储效率(空间复杂度)。
数据结构的三种层次:
1、逻辑结构–抽象层: 主要描述的是数据元素之间的逻辑关系
**集合结构(集):**所有的元素都属于一个总体,除了同属于一个集合外没有其他关系。集合结构不强调元素之间的任何关联性。
**线性结构(表):**数据元素之间具有一对一的前后关系。结构中必须存在唯一的首元素和唯一的尾元素。
**树状结构(树):**数据元素之间一对多的关系
**图状结构(图):**图状结构或网状结构 结构中的数据元素之间存在多对多的关系
2、物理结构–存储层: 主要描述的是数据元素之间的位置关系
描述 | 优点 | 缺点 | |
---|---|---|---|
顺序结构 | 顺序结构就是使用一组连续的存储单元依次存储逻辑上相邻的各个元素 | 只需要申请存放数据本身的内存空间即可,支持下标访问,也可以实现随机访问。 | 必须静态分配连续空间,内存空间的利用率比较低。插入或删除可能需要移动大量元素,效率比较低 |
链式结构 | 链式存储结构不使用连续的存储空间存放结构的元素,而是为每一个元素构造一个节点。节点中除了存放数据本身以外,还需要存放指向下一个节点的指针。 | 不采用连续的存储空间导致内存空间利用率比较高,克服顺序存储结构中预知元素个数的缺点 插入或删除元素时,不需要移动大量的元素。 | 需要额外的空间来表达数据之间的逻辑关系, 不支持下标访问和随机访问。 |
索引结构 | 除建立存储节点信息外,还建立附加的索引表来标节点的地址。索引表由若干索引项组成。 | 是用节点的索引号来确定结点存储地址,检索速度块 | 增加了附加的索引表,会占用较多的存储空间。 |
散列结构 | 由节点的关键码值决定节点的存储地址。散列技术除了可以用于查找外,还可以用于存储。 | 散列是数组存储方式的一种发展,采用存储数组中内容的部分元素作为映射函数的输入,映射函数的输出就是存储数据的位置, 相比数组,散列的数据访问速度要高于数组 | 不支持排序,一般比用线性表存储需要更多的空间,并且记录的关键字不能重复。 |
**3、运算结构–实现层:**主要描述的是如何实现数据结构
- 分配资源,建立结构,释放资源
- 插入和删除
- 获取和遍历
- 修改和排序
每种逻辑结构采用何种物理结构来实现,并没有具体的规定。当一个结构,在逻辑结构中只有一种定义,而在物理结构中却有两种选择,那么这个结构就属于逻辑结构;
数据结构比较:
数据结构 | 优点 | 缺点 |
---|---|---|
数组 | 查询快 | 增删快,大小固定,只能存储单一元素 |
有序数组 | 比无序数组查询快 | 插入慢,删除慢,大小固定,只能存储单一元素 |
栈 | 先进后出的存取方式 | 存取其他项很慢 |
队列 | 先进先出的存取方式 | 存取其他项很慢 |
链表 | 插入快,删除快 | 查找慢 |
二叉树 | 如果树是平衡的,则增删找都很快 | 删除算法复杂 |
红黑树 | 增删找都快,树总是平衡的算法复杂 | 算法复杂 |
2-3-4树 | 增删找都快,树总是平衡的。类似的树对磁盘存储有效 | 算法复杂 |
哈希表 | 如果关键字已知,则存取极快 | 删除慢,如果不知关键字,对存储空间使用不充分 |
堆 | 增删快,对最大数据项存取快 | 对其他项数据存储慢 |
图 | 对现实世界建模 | 有些算法慢且复杂 |
数据结构 | 查找 | 插入 | 删除 | 遍历 |
---|---|---|---|---|
数组 | O(N) | O(1) | O(N) | —— |
有序数组 | O(logN) | O(N) | O(N) | O(N) |
链表 | O(N) | O(1) | O(N) | —— |
有序链表 | O(N) | O(N) | O(N) | O(N) |
二叉树(一般情况) | O(logN) | O(logN) | O(logN) | O(N) |
二叉树(最坏情况) | O(N) | O(N) | O(N) | O(N) |
平衡树(一般情况和最坏情况) | O(logN) | O(logN) | O(logN) | O(N) |
哈希表 | O(1) | O(1) | O(1) | —— |
常见数据结构
数据结构的选择
O符号
O在算法当中表述的是时间复杂度,它在分析算法复杂性的方面非常有用。常见的有:
1、O(1)
:最低复杂度,无论数据量大小,耗时都不变,都可以在一次计算后获得。哈希算法就是典型的O(1)
2、O(n)
:线性,n表示数据的量,当量增大,耗时也增大,常见有遍历算法
3、O(n²)
:平方,表示耗时是n的平方倍,当看到循环嵌循环的时候,基本上这个算法就是平方级的,如:冒泡排序等
4、O(log n)
:对数,通常ax=n,那么数x叫做以a为底n的对数,也就是x=logan,这里是a通常是2,如:数量增大8倍,耗时只增加了3倍,二分查找就是对数级的算法,每次剔除一半
5、O(n log n)
:线性对数,就是n乘以log n,按照上面说的数据增大8倍,耗时就是8*3=24倍,归并排序就是线性对数级的算法
三、Array
1、介绍
数组是一种连续存储线性结构,元素类型相同,大小相等。
可以在内存中连续存储多个元素的结构。在内存分配也是连续的。数组中的元素通过数组下标进行访问,从0开始。
//只声明了类型和长度
数据类型 [] 数组名称 = new 数据类型[数组长度];
//声明了类型,初始化赋值,大小由元素个数决定
数据类型 [] 数组名称 = {数组元素1,数组元素2,......}
大小固定,不能动态扩展(初始化给大了,浪费;给小了,不够用),插入快,删除和查找慢
优点:
查询快
1、按照索引查询元素速度快
2、按照索引遍历数组方便
缺点:
增加删除慢
1、事先必须知道数组的长度
2、空间通常是有限制的,数组的大小固定后就无法扩容了
3、数组只能存储一种类型的数据
4、添加,删除的操作慢,因为要移动其他的元素。
2、适用场景
频繁查询,对存储空间要求不大,很少增加和删除的情况。
3、模拟实现
public class Array {
private int[] intArray;
private int elems;
private int length;
public Array(int max) {
length = max;
intArray = new int[max];
elems = 0;
}
/**
* 添加
* @param value
*/
public void add(int value){
if(elems == length){
System.out.println("error");
return;
}
intArray[elems] = value;
elems++;
}
/**
* 查找
* @param searchKey
* @return
*/
public int find(int searchKey){
int i;
for(i = 0; i < elems; i++){
if(intArray[i] == searchKey)
break;
}
if(i == elems){
return -1;
}
return i;
}
/**
* 删除
* @param value
* @return
*/
public boolean delete(int value){
int i = find(value);
if(i == -1){
return false;
}
for (int j = i; j < elems-1; j++) {
//后面的数据往前移
intArray[j] = intArray[j + 1];
}
elems--;
return true;
}
/**
* 更新
* @param oldValue
* @param newValue
* @return
*/
public boolean update(int oldValue,int newValue){
int i = find(oldValue);
if(i == -1){
return false;
}
intArray[i] = newValue;
return true;
}
/**
* 遍历
*/
public void display(){
for(int i = 0 ; i < elems ; i++){
System.out.print(intArray[i]+" ");
}
System.out.print("\n");
}
/**
* 冒泡排序
* 每趟冒出一个最大数/最小数
* 每次运行数量:总数量-运行的趟数(已冒出)
*/
public void bubbleSort(){
//排序趟数 n-1次就行了
for(int i = 0; i < elems-1; i++){
System.out.println("第"+(i+1)+"趟:");
//每趟次数 (n-i) -1是防止下标越界,后面赋值用到了+1
for(int j = 0; j < elems-i-1; j++){
//控制按大冒泡还是按小冒泡
if(intArray[j] > intArray[j+1]){
int temp = intArray[j];
intArray[j] = intArray[j+1];
intArray[j+1] = temp;
}
display();
}
}
}
/***
* 选择排序
* 每趟选择一个最大数/最小数
* 每次运行数量:总数量-运行的趟数(已选出)
*/
public void selectSort(){
for(int i = 0; i < elems-1; i++) {
//排序趟数 n-1次就行了
int min = i;
for(int j = i+1; j < elems; j++){
//排序次数 每趟选择一个数出来,-1次
if(intArray[j] < intArray[min]){
min = j;
}
}
if(i != min ){
int temp = intArray[i];
intArray[i] = intArray[min];
intArray[min] = temp;
}
display();
}
}
/**
* 插入排序
* 每趟选择一个待插入的数
* 每次运行把待插入的数放在比它大/小后面
*/
public void insertSort(){
int j;
for(int i = 1; i < elems; i++){
int temp = intArray[i];
j = i;
while (j > 0 && temp < intArray[j-1]){
intArray[j] = intArray[j-1];
j--;
}
intArray[j] = temp;
}
display();
}
public void display() {
System.out.println(array.toString());
}
public static void main(String[] args) {
Array array = new Array(10);
array.add(6);
array.add(3);
array.add(8);
array.add(2);
array.add(11);
array.add(5);
array.add(7);
array.add(4);
array.add(9);
array.add(10);
// array.bubbleSort();
// array.selectSort();
array.insertSort();
// array.display();
// System.out.println(array.find(4));
// System.out.println(array.delete(1));
// array.display();
// System.out.println(array.update(2,6));
// array.display();
}
}
四、Stack
1、介绍
-
栈(stack)又称为堆栈或堆叠,栈作为一种数据结构,
它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶
-
java中Stack是Vector的一个子类,只定义了默认构造函数,用来创建一个空栈。
-
栈是元素的集合,其包含了两个基本操作:
push 操作可以用于将元素压入栈,
pop 操作可以将栈顶元素移除。
-
遵循先进后出原则(LIFO, Last In First Out)。
-
时间复杂度:
索引:
O(n)
搜索:
O(n)
插入:
O(1)
移除:
O(1)
2、适用场景:
栈常应用于实现递归功能方面的场景,例如斐波那契数列。
3、模拟实现
public class Stack {
//小贴士:通常可以利用栈实现字符串逆序,还可以利用栈判断分隔符是否匹配,如<a[b{c}]>,可以正进反出,栈为空则成功。
/**基于数组实现的顺序栈,连续存储的线性实现,需要初始化容量**/
//固定数据类型
//private int[] array;
//动态数据类型
private Object[] objArray;
private int maxSize;
private int top;
public Stack() {
}
public Stack(int maxSize) {
if(maxSize > 0){
objArray = new Object[maxSize];
this.maxSize = maxSize;
top = -1;
}else{
throw new RuntimeException("初始化大小错误:maxSize=" + maxSize);
}
}
/**
* 入栈
* @param obj
*/
public void objPush(Object obj){
//扩容
grow();
//++在前表示先运算再赋值,优先级高,在后表示先赋值再运算,优先级低
objArray[++top] = obj;
}
/**
* 出栈
* @return
*/
public Object objPop(){
Object obj = peekTop();
//声明原顶栈可以回收空间(GC)
objArray[top--] = null;
return obj;
}
/**
* 查看栈顶
* @return
*/
public Object peekTop(){
if(top != -1){
return objArray[top];
}else{
throw new RuntimeException("stack is null");
}
}
/**
* 动态扩容
*/
public void grow(){
// << 左移运算符,1表示乘以2的1次方
if(top == maxSize-1){
maxSize = maxSize<<1;
objArray = Arrays