📖 前言
在学习数据结构与算法之前, 我们要知道什么是数据结构?为什么要学数据结构与算法?
• 数据结构就是研究数据如何在计算机中进行组织和存储, 使我们可以高效的获取数据和修改数据.
• 要写出好的程序一定要学好数据结构与算法, 我们脑海里要时刻有如下这个公式:
数据结构 + 算法 = 程序
• 在学习数据结构过程中,我们要形成一种意识:并不是简单的实现,而是强调比较和优化。
数据结构可以分为三类:
• 线性结构:数组、队列、栈、链表、哈希表...
• 树型结构:二叉树、二分搜索树、AVL树、红黑树、堆、Trie、线段树、并查集...
• 图结构: 邻接矩阵、邻接表
🎀 数组
概念:
✎ 一组相同数据类型的数或相同类型数的集合.
数组在内存中如何分配:
✎ 分配的是连续的空间, 因此在创建数组时要指定数组的大小, 数组一旦创建就不能修改.
如何声明和定义数组:
✎ int [] arr = new int[10] ;
数组操作:
✎ 增删改查 (crud) 【C:Create(创建) Retrieve(查询) Update(更新) Delete(删除)】
如何使用数组:
✎ 通过数组的下标(索引) 下标是从0开始的,至数组中元素的个数(数组的长度)-1 arr.length
常见问题:
✎ 空数组---[ ] 空对象---null
NullPointException 空指针异常(要知道谁为空)
ArraysIndexOutOfBoundsException 下标越界
数组优缺点:
✎ 优:快速查询! 缺:删除/添加元素慢!
常见数组有:
✎ 字符串、哈希表、对象数组.
🎀 自建数组 MyArray
创建属于自己的数组 ----- 注意:是基于java中的数组进行二次封装.
✰ 定义属性 (数据容器、实际存放元素个数、容积)
public class MyArray<T> { //定义数据容器 private T[] data; //实际存放元素的个数 private int size; //容积 capacity private int capacity;
✰ 创建构造方法 (无参构造方法和有参构造方法)
public MyArray() { this(100); } public MyArray(int capacity) { //先判断容积是否合法,不合法默认容积为一个值 if (capacity <= 0) { capacity = 100; } this.capacity = capacity; this.size = 0; this.data = (T[]) new Object[this.capacity]; }
实现MyArray类的功能 (常用方法)
//获取数组容积 public int getCapacity() { return this.capacity; } //判断是否为空 public boolean isEmpty() { return this.size == 0; } //获取实际存放元素的大小 public int getSize() { return this.size; }
✰ 数组操作(增删改查)
📌 添加元素
//添加元素 public void add(int index, T ele) { if (index < 0 || index > this.size) { throw new IllegalArgumentException("index is invalid!"); } //判断是否扩容 if (index == this.capacity) { resize(this.capacity * 2); } //判断哪些元素后移 for (int i = this.size; i >= index; i--) { this.data[i + 1] = this.data[i]; } this.data[index] = ele; this.size += 1; } //在头部添加 public void addHead(T ele) { add(0, ele); } //在尾部添加 public void addTail(T ele) { add(this.size, ele); }
📌 删除元素
//删除操作 public T remove(int index) { if (index < 0 || index > this.size) {//先判断索引是否合法 throw new IllegalArgumentException("index is invalid!"); } //保存删除元素 T delVal = this.data[index]; for (int i = index + 1; i < this.size; i++) { this.data[i - 1] = data[i]; } this.size--; if (this.size <= (this.capacity / 4) && capacity / 2 > 1) { resize(this.capacity / 2); } return delVal; } public T removeHead() {//删除头部元素 return remove(0); }
修改元素
//修改操作 public void updateByIndex(int index, T ele) { if (index < 0 || index > this.size) {//先判断索引是否合法 throw new IllegalArgumentException("index is invalid!"); } this.data[index] = ele; }
查询元素
//查询某个元素是否存在 public boolean contains(T ele) { for (int i = 0; i < this.size; i++) { if (this.data[i].equals(ele)) { return true; } } return false; } //查询元素在数组中是否存在,如果存在,返回索引,否则返回-1 public int findIndex(T ele) { for (int i = 0; i < this.size; i++) { if (this.data[i].equals(ele)) { return i; } } return -1; }
完整代码
package com.learn.study1; public class MyArray<T> { //定义数据容器 private T[] data; //实际存放元素的个数 private int size; //容积 capacity private int capacity; public MyArray() { this(100); } public MyArray(int capacity) { if (capacity <= 0) { capacity = 100; } this.capacity = capacity; this.size = 0; this.data = (T[]) new Object[this.capacity]; } //获取数组容积 public int getCapacity() { return this.capacity; } //判断是否为空 public boolean isEmpty() { return this.size == 0; } //获取实际存放元素的大小 public int getSize() { return this.size; } //添加元素 public void add(int index, T ele) { if (index < 0 || index > this.size) { throw new IllegalArgumentException("index is invalid!"); } //判断是否扩容 if (index == this.capacity) { resize(this.capacity * 2); } //判断哪些元素后移 for (int i = this.size; i >= index; i--) { this.data[i + 1] = this.data[i]; } this.data[index] = ele; this.size += 1; } //在头部添加 public void addHead(T ele) { add(0, ele); } //在尾部添加 public void addTail(T ele) { add(this.size, ele); } //修改操作 public void updateByIndex(int index, T ele) { if (index < 0 || index > this.size) {//先判断索引是否合法 throw new IllegalArgumentException("index is invalid!"); } this.data[index] = ele; } //查询某个元素是否存在 public boolean contains(T ele) { for (int i = 0; i < this.size; i++) { if (this.data[i].equals(ele)) { return true; } } return false; } //查询元素在数组中是否存在,如果存在,返回索引,否则返回-1 public int findIndex(T ele) { for (int i = 0; i < this.size; i++) { if (this.data[i].equals(ele)) { return i; } } return -1; } //删除操作 public T remove(int index) { if (index < 0 || index > this.size) {//先判断索引是否合法 throw new IllegalArgumentException("index is invalid!"); } //保存删除元素 T delVal = this.data[index]; for (int i = index + 1; i < this.size; i++) { this.data[i - 1] = data[i]; } this.size--; if (this.size <= (this.capacity / 4) && capacity / 2 > 1) { resize(this.capacity / 2); } return delVal; } public T removeHead() {//删除头部元素 return remove(0); } //重写toString @Override public String toString() { //[1,2,3,4] StringBuilder sb = new StringBuilder("["); for (int i = 0; i < this.size; i++) { sb.append(this.data[i].toString()); if (i != this.size - 1) { sb.append(","); } } sb.append("]"); return super.toString(); } private void resize(int newCapacity) { System.out.println("resize操作,新的容积:" + newCapacity); T[] newData = (T[]) new Object[newCapacity]; //空间复杂度 O(n) // 将原数组中的数组复制到新数组中 // 时间复杂度O(n) for (int i = 0; i < this.size; i++) { newData[i] = this.data[i]; } // 更新容器 this.data = newData; // 更新容积 this.capacity = newCapacity; } public static void main(String[] args) { } }
🎀 复杂度分析和均摊复杂度
• 在算法领域,使用复杂度分析的内容来评价写的代码性能如何。
📌时间复杂度
- 通常情况下我们只需要简单的时间复杂度分析就OK!
- 大O描述的是算法的运行时间和输入数据之间的关系 (数据量要足够的大,趋于无穷)
- 通常情况有:O(1),O(n),O(Ign),O(nIogn),O(n^2)
• 在计算时需要忽略常数,如:
T = 2*n+2 -------- O(n)
T = 20000*n+50000 -------- O(n)
T = 5 * n * n + 5 -------- O(n^2)
这时很多小伙伴就问了:比如n取10, T2明显时间大于T3啊,注意:此时我们要考虑n趋于无穷的情况 (而n趋于无穷时常数就可以忽略)
综合来看:
- 增:O(n)
- 删:O(n)
- 改:已知索引O(1);未知索引O(n)
- 查:已知索引O(1);未知索引O(n)
📌均摊时间复杂度
resize O(n)
假设capacity = n , n+1次addLast ,触发resize(扩容),一共进行了2n+1次操作,平均每次addLast操作进行两次操作,这样均摊计算:时间复杂度为O(1)
注意:
当我们同时进行 addLast 和 removelast 操作时,如果过于着急会出现均摊度的震荡问题,此时我们可以在缩容的时候缓一点。