继网络协议之大集合完结之后,我觉得很有必要再来一次数据结构的大集合。对于个人来说可以增强理解,如有问题,欢迎指正,多谢。话不多说,现在开始
数据结构
1- 数据结构概述
-
数据结构是计算机的存储、组织方式。
-
数据结构分为:
-
线性结构
- 线性表、数组、链表、栈、队列、哈希表等
-
树形结构
- 二叉树、AVL树、红黑树、堆、Trie、哈夫曼树、并查集
-
图形结构
- 邻接矩阵
- 邻接表
-
2- 线性结构数组
2-1 线性表
-
线性表是具 n 个相同类型元素的有限序列(n > = 0)。
-
线性表中除了第一个元素(首节点)之外 ,其他任何元素都有直接前驱,除了最后一个元素(尾节点)外,其他任何元素都有直接后继。
-
常见的线性表
- 数组
- 链表
- 栈
- 队列
- 哈希表
2-2 数组
-
数组存在一个致命的缺点
无法动态修改容量。 -
数组是一种
顺序存储的线性表,所有元素的内存地址是连续的就是顺序存储 -
每一个int类型的数据占4个字节,所以 int[] arrays = new int[]{11,32,19} 在计算机中的存储图为:
int[] arrays = new int[]{11,32,19}
2-3 动态数组代码实现
-
使用
java实现动态数组package com.zh.线性表.动态数组; /** * @Author superMonkey * @Date 2022/9/22 15:02 * @Description TODO */ public class ArrayList<T> { private int size; private T[] dynamicArrays; private static final int DEFAULT_LENGTH = 10; public ArrayList() { this(0); } public ArrayList(int len) { //创建数组时 if (len <= 0){ dynamicArrays = (T[]) new Object[DEFAULT_LENGTH]; }else { dynamicArrays = (T[]) new Object[len]; } } /** * 获取元素数量 * size控制了使用者能够访问的下标范围是 [0 , size) , 所以元素访问直接返回size * @return */ public int size(){ return size; } /** * 是否为空 * 如果size == 0 说明可以访问数组范围是 [0 , 0) , 这样的表示说明不能访问任何元素, 所以当前数组为空。 */ public boolean isEmpty(){ return size == 0; } /** * 在指定位置添加元素 * * @param element * @return */ public int add(int index, T element) { //首先判断index的访问是不是在 可以访问的访问之内 //正常来说可以访问的访问应该是 [0 , size) , 但是这里的判断是可以允许index == size , 因为这样就可以兼容在数据末尾添加元素 if (index < 0 || index > size) { throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index); } //扩容的方法 , 因为你添加了一个元素,所以要保证动态数组能够容下 //扩容的方法回判读数组加上一个元素之后是否能够存下,如果能能够存下,就不扩容了 expandCapacity(size + 1); //要想在index上增加一个元素就必须将 index 已经index之后的所有元素向后移动,然后在把元素放到index上 //我们从数组的最后一个元素开始往后移动 , 如果是在数组末尾添加这个元素,不进入for循环,所以末尾添加元素效率高 for (int i = size - 1; i >= index; i--) { //实现了元素后移一位 dynamicArrays[i + 1] = dynamicArrays[i]; } //再让腾出位置来的index上的元素等于我们想要添加的值 dynamicArrays[index] = element; size++; return size; } /** * 在末尾添加元素 * * @param element * @return */ public int add(T element) { add(size, element); return size; } /** * 扩容 * @param length */ public void expandCapacity(int length){ //length是新数组的长度,如果长度小于已存在的动态数组的长度,直接结束,不需要扩容,因为动态数组长度是够的 if (length <= dynamicArrays.length) return; int oldLength = dynamicArrays.length; //将数组扩大,将原来的值左移2位,就是扩到到原来的两倍,jdk11是 >> 1 int newLength = oldLength + (oldLength >> 2); //将老数组的值全部赋值给新数组 Object[] newArrays = new Object[newLength]; for (int i = 0; i < size; i++) { newArrays[i] = dynamicArrays[i]; } //修改私有元素动态数组的引用指向,指向新数组 dynamicArrays = (T[]) newArrays; System.out.println(oldLength + "扩容到" + dynamicArrays.length); } /** * 删除元素 * @param index * @return */ public Object delete(int index) { //判读需要删除的位置是否合理 if (index < 0 || index >= size){ throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index); } //需要将要删除的位置,也就是index上的元素变成 index + 1 上的元素,依次类推 Object old = dynamicArrays[index]; for (int i = index + 1; i < size; i++) { dynamicArrays[i - 1] = dynamicArrays[i]; } //将数组最后一个位置上的元素设置为空,同时 --size 将size减去了1,那么允许访问的范围就减小了1 dynamicArrays[--size] = null; return old; } /** * 清空所有元素 * * @return */ public void clear() { //将动态数组所有可以访问的位置上的元素设置成null for (int i = 0; i < size; i++) { dynamicArrays[size] = null; } // seize = 0 ,那么动态数组就相当于没有任何位置是可以访问的 size = 0; } /** * 修改元素 * * @param index * @param element * @return */ public Object set(int index, Object element) { if (index < 0 || index >= size){ throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index); } Object old = dynamicArrays[index]; dynamicArrays[index] = (T) element; return old; } /** * 获取元素 * * @param index * @return */ public Object get(int index) { if (index < 0 || index >= size){ throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index); } return dynamicArrays[index]; } /** * 查看元素位置 * @return */ public int indexOf(Object element){ for (int i = 0; i < size; i++) { if (element.equals(dynamicArrays[i])){ return i; } } return -1; } /** * 是否包含某个元素 * 调用查看元素位置,返回 -1 说明不存在该元素 */ public boolean contains(T element){ return indexOf(element) != -1; } @Override public String toString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("size:").append(size).append(" ["); for (int i = 0; i < size; i++) { if (i != 0){ stringBuilder.append(", "); } stringBuilder.append(dynamicArrays[i]); } stringBuilder.append("]"); return stringBuilder.toString(); } }
动态数组中dynamicArrays是一个在创建时就固定的数组,长度并不会改变。而是使用了size控制访问的范围。在dynamicArrays的范围上,移动size来当作数组的末尾值,这样的就实现了动态数组。
3- 线性结构链表(Linked List)
- 动态数组存在一个明显缺点,扩容后,造成内存空间浪费。
- 想要实现用多少内存就申请多少空间,链表就可以办到。
3-1 链表概述
-
链表是一种
链式存储的线性表,所有元素的内存地址不一定是连续的。 -
链表存储元素:
-
链表存储元素是在节点对象(Node)中存储元素,这个节点 对象不止存储本身的元素,还存储了下一个节点对象的内存地址。
-
3-2 使用java程序实现链表
-
链表中,头节点的内存地址没有被指向,尾节点中指向下一个节点的内存地址指向null。
-
链表设计思路
1、链表里面因该有两个元素,一个是size(节点的个数),一个是first(指向第一个节点的地址),在节点里面应该也有两个元素,一个是element(表示元素内容),还有一个就是next(指向下一个元素的内存地址),需要使用到一个节点时,再去分配这个节点的内存空间。并且这个节点对象建议放做成内部类
package com.zh.线性表.动态数组; /** * @Author superMonkey * @Date 2023/1/7 23:34 * @Description TODO */ public class LinkList<T> { private int size; private Node first; private static class Node<T>{ T element; Node next; } private static class Node<T>{ T element; Node next; public Node(T element , Node next){ this.element = element; this.next = next; } } } 2.链表的方法大部分和动态数组一样,所以需要提取一个公共类,但是链表和动态数组大部分方法实现内容都不一样,所以公共类就不合适,需要一个接口来标识。
package com.zh.线性表.动态数组; /** * @Author superMonkey * @Date 2023/1/8 0:08 * @Description TODO */ public interface List<T> { int size(); // 元素的数量 boolean isEmpty(); // 是否为空 boolean contains(T element); // 是否包含某个元素 int add(T element); // 添加元素到最后面 T get(int index); // 返回index位置对应的元素 T set(int index, T element); // 设置index位置的元素 int add(int index, T element); // 往index位置添加元素 T remove(int index); // 删除index位置对应的元素 int indexOf(T element); // 查看元素的位置 void clear(); // 清除所有元素 } - 但其实ArrayList和LinkLisk还是有很多方法和参数是一样的,所以可以用一个抽象类在List接口和ArrayList,LinkLisk之间隔一下,实现公共的一些方法。
package com.zh.线性表.动态数组; /** * @Author superMonkey * @Date 2023/1/8 0:15 * @Description TODO */ public abstract class AbstractList<T> implements List<T>{ protected int size; /** * 获取元素数量 * size控制了使用者能够访问的下标范围是 [0 , size) , 所以元素访问直接返回size * @return */ public int size(){ return size; } /** * 是否为空 */ public boolean isEmpty(){ return size == 0; } /** * 在末尾添加元素 * * @param element * @return */ public int add(T element) { add(size, element); return size; } /** * 是否包含某个元素 * 调用查看元素位置,返回 -1 说明不存在该元素 */ public boolean contains(T element){ return indexOf(element) != -1; } } -
LinkList具体实现
```java
package com.zh.线性表.动态数组;
/**
* @Author superMonkey
* @Date 2023/1/7 23:34
* @Description TODO
*/
public class LinkList<T> extends AbstractList<T>{
private int size;
//这个first才是第一个节点,我们在第一个节点之前有一个LinkLisk
private Node first;
private static class Node<T>{
T element;
Node next;
public Node(T element , Node next){
this.element = element;
this.next = next;
}
}
/**
* 清空元素
* 清空元素只要两步,size = 0 ; first = null
* first 指向第一个节点对象,把 first 指向 null , 第一个节点对象就会销毁,第一个节点对象里的next本来是指向第二个节点对象的内存地址
* 但是第一个节点对象被销毁了,所以第一个节点的next就不存在了,那么第二个节点对象就回被销毁,同理,清空了所有的元素。
*/
@Override
public void clear() {
size = 0;
first = null;
}
/**
* node方法用于获取index位置的节点
* 此方法仅仅只是在链表内部使用,不对外公开
* @param index
* @return
*/
private Node<T> node(int index) {
//检查下标是否合理 , 这段代码可以抽出来,变成一个单独检查下标的方法放到抽象类中,因为这个方法在ArrayList和LinkList是一样的
if (index < 0 || index >= size){
throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index);
}
//从第0个节点开始找,找到index,找到后返回index对应的节点
Node<T> node = this.first;//获取第0个节点
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
@Override
public T get(int index) {
//调用 node 方法,所以不需要再判断下标是否合理
return node(index).element;
}
@Override
public T set(int index, T element) {
Node<T> node = node(index);
T old = node.element;
node.element = element;
return old;
}
/**
* 在 index 处添加一个元素。先新建一个节点对象,将元素element放到新建里节点对象里,
* 此时需要将 index 前面一个节点对象的 next 指向新的节点,将新的节点中next指向前一个节点对象原来的next指向的节点
* @param index
* @param element
* @return
*/
@Override
public int add(int index, T element) {
if (index < 0 || index > size){
throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index);
}
if (index == 0) {
//先将新节点的next指向原来的first,再将first指向新节点
first = new Node(element, first);
}else {
//获取index前一个节点
Node<T> previous = node(index - 1);
//创建一个新节点,将前一个节点的next交给新节点的next,并将前一个节点的next指向新节点。
previous.next = new Node<>(element , previous.next);
}
//链表长度 + 1
size++;
return size;
}
/**
* 获得 index - 1 的node , 将index - 1 的node中的next 指向 index 对应node的next
* @param index
* @return
*/
@Override
public T remove(int index) {
if (index < 0 || index > size){
throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index);
}
Node<T> node = first;
if (index == 0){
first = first.next;
}else {
Node<T> previous = node(index - 1);
node = previous.next;//被删除的节点
previous.next = node.next;
}
size--;
return node.element;
}
@Override
public int indexOf(T element) {
//element是范性,要用equals比较,所以这里需要考虑null的情况。
if (element == null) {
Node<T> node = first;
for (int i = 0; i < size; i++) {
if (node.element == element) return i;
node = node.next;
}
}else {
Node<T> node = first;
for (int i = 0; i < size; i++) {
//node.element有可能是null,但这里element肯定不是空
if (element.equals(node.element)) return i;
node = node.next;
}
}
return -1;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("size:").append(size).append(" [");
Node node = first;
for (int i = 0; i < size; i++) {
if (i != 0){
stringBuilder.append(", ");
}
stringBuilder.append(node.element);
node = node.next;
}
stringBuilder.append("]");
return stringBuilder.toString();
}
}
```