什么是数据结构?
数据结构是一门研究非数值计算的程序设计问题中计算机的操作对象(数据)以及它们之间的关系和操作等的学科。
数据结构的三方面研究内容:
- 数据的逻辑结构:数据之间的逻辑关系,与数据的存储无关,独立于计算机;
- 数据的**(物理)存储结构**:数据在计算机中的具体存储实现方式,依赖于计算机;
- 数据的操作实现算法:按一定逻辑结构组织的数据所具有的各种操作,其对应算法如何在具体存储结构上的实现。
- 数据结构和算法在程序中的应用:灵活的使用数据结构可以带来高效率的运行与存储。
在现有的程序产品中就有很多例子。
实例:
- 通讯录:使用上下排布的线性表
- 导航:使用图论算法,其中自动寻路则使用到深度优先遍历,广度优先遍历
- 医院排号:队列算法
- 数据库:B+树实现
- 塔防游戏:优先队列算法
- JVM函数栈:由栈实现
- 网络路由:图论算法
故而:有数据扎堆的地方都有数据结构,有数据结构的地方都有算法
数据结构的作用
首先:我们需要知道:
程序 = 数据结构 + 算法
在程序的制作和后期的工作中,数据结构和算法起着工作效率的决定性作用
同样的数据结构可以说是编程的灵魂
它只是给程序开发人员一种开发思路,讲的主要是已经成熟的编程思想和算法,几乎适用于所有开发语言
在程序开发的过程中,我们更多的是处理非数值计算问题的数学模型不再是数学方程,而是诸如线性表、树、图之类的数据结构。因此,可以说数据结构课程主要是研究非数值计算的程序设计问题中所出现的计算机操作对象以及它们之间的关系和操作的学科。
更为实际的:数据结构能够锻炼编程能力,提高变成思想,扩展思路,提高实际应用能力,获取相应的校招优势
为将来的人工智能、大数据、云计算等领域奠定基础
处理数据的优势:
-
将离散的数据规整化一
-
将数据存储到计算机
-
基于关系上对数据进行具体操作,增删查改
链表来存储二叉树 -
数据结构的实际应用
数据的应用方式决定了底层使用什么来存储
同时学习数据结构更利于我们深究进阶底层源码:
- ArrayList
- LinkedList
- HashMap
- HashSet…
- Collection
- Map
- ……
数据之间的关系
数据之间的关系(逻辑结构):
- 线性结构
数据元素之间一对一关系 - 树形结构
数据元素之间一对多关系 - 图形结构
- 数据元素之间多对多关系
数据关系在计算上的存储(物理结构):
- 顺序存储结构
本质就是一个数组
空间上连续,一块连续的空间开辟
优点:空间连续,查询效率高
缺点:内存利用率较低,增删效率低 - 链式存储结构
开辟一组随机的空间存储数据,
优点:内存利用率高,使用节点,增删效率高
缺点:查询效率低
数据存储模型:
- 线性结构
- 线性表顺序存储结构
- 线性表链式存储结构
- 树形结构
- 树形顺序存储结构
- 树形链式存储结构
二叉树
- 图形结构
- 图形顺序存储结构
- 图形链式存储结构、
算法概述
算法:
解决问题的思路
可以由自然语言描述,流程图描述,伪代码描述
判断算法优劣的标准:
程序运行时间和占用空间
度量方法:
- 事后统计法:
弊端:
事先编好程序,运行,当数据量较大时,将花费大量的时间和精力。
时间较依赖计算机硬件环境和软件环境 - 事前分析法:
主要决定因素:
算法本身的策略和方法
输入规模
软硬件环境
为了更好的判断算法优劣,设定时间复杂度和空间复杂度作为评判标准
时间复杂度:
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)=O(f(n))。它表示随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,作算法的渐进时间复杂度。简称时间复杂度。其中f(n)是问题规模n的某个函数
空间复杂度:
空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))。比如直接插入排序的时间复杂度是O(n^2),空间复杂度是O(1) 。而一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息。
重点强调时间复杂度
常见时间复杂度:
-
常数阶O(1)
-
线性阶O(n) O(n+m) 区别于不同的输入规模
-
平方阶O(n²) O(nm) 区别于不同的输入规模
-
对数阶O(logn)
常见阶比较:
静态数组与动态数组
Java内置静态数组的特点(优点):
-
数组的长度一旦确定不能更改
int[] arr1 = new int[5] ;
int[] arr2 = new int[] {1, 2, 3, 4, 5} ; -
数组只能存储同一类型的数据
但不是完全同一类型,而且向下兼容
double[] arr3 = new double[5] ;
向下兼容int bit
-
数组中每个存储空间地址是连续且相等的
-
数组提供索引的方式访问元素
Java内置数组的缺点:
-
长度不可变,难以扩容
-
增删困难,访问很快,效率很高
牵一发动全身
-
数组仅有length一个属性,没有其他方法
将静态数组变成动态数组:
-
扩容的操作
-
增删的操作
时间复杂度为O(n)
查改
时间复杂度为O(1)
-
对于仅有length属性
使用面向对象的思想将数组再次进行封装
E[] data
int size = 2
将静态数组设计成动态数组
1、容量不够:扩容
2、元素的增删
3、增添其他属性,并进行封装
动态数组就是顺序存储结构具体实现的核心思想
主要涉及有:
- 线性表
- 栈
- 双端栈
- 队列
- 循环队列
- 双端队列
线性表
线性表的底层就是动态数组
故而在理解动态数组构造的基础上,我们可以自制线性表
- 首先是List接口的定义
package P1.Interface;
import java.util.Comparator;
public interface List<E> extends Iterable<E> {
//默认在表尾添加一个元素
public void add(E element);
//在指定索引处添加元素
public void add(int index, E element);
//删除指定元素
public void remove(E element);
//删除指定索引处的元素,并返回原先值
public E remove(int index);
//获取指定索引处的元素
public E get(int index);
//修改指定索引index处的值为element 并返回原先的值
public E set(int index, E element);
//获取线性表中的元素个数
public int size();
//查看元素第一次出现的索引位置(从左到右)
public int indexOf(E element);
//判断是否包含元素
public boolean contains(E element);
//判断线性表是否为空
public boolean isEmpty();
//清空线性表
public void clear();
//按照比较器的内容进行排序
public void sort(Comparator<E> c);
//获取子线性表 原线性表中[fromIndex, toIndex)这个部分
public List<E> subList(int fromIndex, int toIndex);
}
细节注意:
Iterable为可迭代接口,实现线性表的可迭代性
同时data不能直接引用外部传入的数组arr
由于数据不能通过外界直接操作,所以不能直接给其数据地址,只能将数据复制一份,传给外界操作
- 构写ArrayList
package P2.linearStructure;
import P1.Interface.List;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Objects;
//自定义的线性表的顺序存储结构
public class ArrayList<E> implements List<E> {
//数组的容器 data.length 指的就是当前数组的容量
private E[] data;
//元素的个数 size == 0 表示线性表为空表 size == data.length 表示线性表满了
//size 还表示新元素默认尾部添加是所存储的索引
private int size;
//默认容量
private static int DEFAULT_CAPACITY = 10;
//默认构造函数:创建一个默认容量为10 的线性表
public ArrayList() {
data = (E[]) new Object[DEFAULT_CAPACITY];
size = 0;
}
//指定默认容量的构造函数:创建一个指定容量的线性表
public ArrayList(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("capacity must be greater than zero"); //抛出非法参数异常
}
DEFAULT_CAPACITY = capacity;
data = (E[]) new Object[DEFAULT_CAPACITY];
size = 0;
}
//指定数组的构造函数:传入一个数组将该数组封装称为一个线性表
//[1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
public ArrayList(E[] arr) {
//data不能直接引用外部传入的数组arr
//否则外部对arr的修改会引起ArrayList内部的一些问题
//data是ArrayList内部的数据
if (arr == null || arr.length == 0) {
throw new IllegalArgumentException("arr can not be null");
}
data = (E[]) new Object[DEFAULT_CAPACITY];
for (int i = 0; i < arr.length; i++) {
add(arr[i]);
}
}
@Override
public void add(E element) {
add(size, element); //复用add
}
@Override
public void add(int index, E element) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("add index cross the border");
}
//判断线性表是否是满状态
if (size == data.length) {
resize(2 * data.length);
}
//设置i从size-1开始到index,逐一向后移动元素
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
//将新元素插入到指定位置
data[index] = element;
//最大长度+1
size++;
}
//扩容或缩容 操作 不应该向外界提供
private void resize(int newLen) {
E[] newData = (E[]) new Object[newLen];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
@Override
public void remove(E element) {//删除指定元素 仅删除依次 && 删除所有指定元素
int index = indexOf(element);
if (index != -1) {
remove(index);
}
}
@Override
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("remove index out of range");
}
//先保存要删除的值
E ret = data[index];
//移动元素
for (int i = index + 1; i < size; i++) {
data[i - 1] = data[i];
}
size--;
//什么时候去缩容 当容量到达默认容量则缩容的意义就变得很小
//当小宇等于默认容量时,就不需要缩容了
//条件1、当前有效元素是容量的1/4
//条件2、当前容量不得小于等于默认容量
if (size == data.length / 4 && data.length > DEFAULT_CAPACITY) {
resize(data.length / 2);
}
return ret;
}
@Override
public E get(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("get index out of range ");
}
return data[index];
}
@Override
public E set(int index, E element) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("set index out of range ");
}
E ret = data[index];
data[index] = element;
return ret;
}
@Override
public int size() {
return size;
}
//额外添加函数:获取当前线性表的容量
private int getCapacity() {
return data.length;
}
@Override
public int indexOf(E element) {
/*
== 比较的是,主要看等号两边是什么
== 两边是基本数据类型,比较的是值
byte short int long
float double
char boolean
== 两边是引用数据类型,比较的是地址
数组 字符串 其他类对象
*/
for (int i = 0; i < size; i++) {
if (data[i].equals(element)) {
return i;
}
}
return -1;
}
@Override
public boolean contains(E element) {
return indexOf(element) != -1;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void clear() {
data = (E[]) new Object[DEFAULT_CAPACITY];
}
@Override
public void sort(Comparator<E> c) {
if (c == null) {
throw new IllegalArgumentException("comparator can not be null");
}
for (int i = 1; i < size; i++) {
E e = data[i];
int j = i;
for ( j = i; j > 0 && c.compare(data[j - 1], e) > 0; j--) {
data[j] = data[j - 1];
}
data[j] = e;
}
}
@Override
public List<E> subList(int fromIndex, int toIndex) {
if (fromIndex < 0 || toIndex >= size ||fromIndex > toIndex) {
throw new IllegalArgumentException("must 0 <= fromIndex <= toIndex <= size");
}
ArrayList<E> list = new ArrayList<>();
for (int i = fromIndex; i <= toIndex; i++) {
list.add(data[i]);
}
return list;
}
@Override
//equals比较两个ArrayList是否相等
public boolean equals(Object o) {
//条件1、判空
if (o == null) {
return false;
}
//条件2、判断是否为自己
if (this == o) {
return true;
}
//条件3、判断类型是否相同
if (o instanceof ArrayList) {
//条件4、按照自己的逻辑进行比较
ArrayList<E> other = (ArrayList<E>) o;
//条件5、先比较有效元素个数
if (this.size != other.size) {
return false;
}
//条件6、有效元素个数相等,逐个比较元素
for (int i = 0; i < size; i++) {
if (!data[i].equals(other.data[i])) {
return false;
}
}
return true;
}
return false;
}
/*
[1, 2, 3, 4, 5, 6]
Arrays.toString(arr); Arrays 类的一个静态方法
*/
@Override
public String toString() {
//为什么用stringbuilder拼接,因为用string拼接会产生不必要的字符类
StringBuilder sb = new StringBuilder();
sb.append('[');
if (isEmpty()) {
sb.append(']');
} else {
for (int i = 0; i < size; i ++) {
sb.append(data[i]);
if (i == size - 1) {
sb.append(']');
} else {
sb.append(',');
sb.append(' ');
}
}
}
return sb.toString();
}
@Override
//迭代器的作用,不论底层是什么逻辑结构,讲其中元素逐个提取出
//获取当前数据结构/容器 的 迭代器
//通过迭代器 我们能更方便的挨个取出 每个元素
//同时 Arraylist实现Iterator 接口可以使当前的 数据结构/容器 被foreach循环遍历
public Iterator<E> iterator() {
return new ArraylistIterator();
}
//创建一个属于ArrayList的迭代器
class ArraylistIterator implements Iterator<E> {
private int cur = 0;
@Override
public boolean hasNext() {
return cur < size;
}
@Override
public E next() {
return data[cur++];
}
}
}
细节注意:
插入排序的优化:
for (int i = 1; i < size; i++) {
E e = data[i];
int j = i;
for ( j = i; j > 0 && c.compare(data[j - 1], e) > 0; j--) {
data[j] = data[j - 1];
}
data[j] = e;
}
比较器的使用:
public void sort(Comparator<E> c) {
if (c == null) {
throw new IllegalArgumentException("comparator can not be null");
}
for (int i = 1; i < size; i++) {
E e = data[i];
int j = i;
for ( j = i; j > 0 && c.compare(data[j - 1], e) > 0; j--) {
data[j] = data[j - 1];
}
data[j] = e;
}
}