数据结构基础
计算机科学中,数据结构是一种数据组织、管理和存储的格式,它可以帮助我们实现对数据高效的访问和修改。更准确地说,数据结构是数据值的集合,可以体现数据值之间的关系,以及可以对数据进行应用的函数和操作。
通俗地说,我们需要去学习在计算机中如何去更好地管理我们的数据,才能让我们对我们的数据结构控制更加灵活。
线性表
线性表是最基本的一种数据结构,它是表示一组相同类型数据的有限序列,你可以把它与数据进行参考,但它并不是数组,线性表是一种表接口,它能够支持数据的插入、删除、更新、查询等,同时数组可以随机存放在数组中任意位置,而线性表只能依次有序排列,不能出现空隙,因此我们需要进一步的设计。
顺序表
将数据以此存储在连续的整块物理空间中,这种存储结构称为顺序存储结构,而以这种方式实现的线性表,我们称为顺序表。
同样的,表中的一个个体称之为元素,元素左边的元素称为前驱,右边的元素称为后驱。
借助数组实现,然后对方法进行封装,实现增删改查。
目标:以数组为底层,编写以下抽象类的具体实现
先定义一个抽象类,再继承实现
/**
* 线性表抽象类
* @param <E> 存储的元素类型
*/
public abstract class AbstractList<E> {
/**
* 获取表的长度
* @return顺序表的长度
*/
public abstract int size();
/**
*添加一个元素
* @param e 元素
* @param index 要添加的位置
*/
public abstract void add(E e, int index);
/**
* 移除指定位置的元素
* @param index 位置
* @return 移除的元素
*/
public abstract E remove(int index);
/**
* 获取指定位置的元素
* @param index 位置
* @return 元素
*/
public abstract E get(int index);
}
public class ArrayList<E> extends AbstractList<E> {
//底层数组
private Object[] arr = new Object[1]; //初始大小为20,后面会动态增长
//长度
private int size = 0;
@Override
public int size() {
return size;
}
@Override
public void add(E e, int index) {
//位置是否合法(添加异常)
if(index > size) throw new IllegalArgumentException("非法的插入位置");
//如果要扩容,就扩容
if (size >= arr.length){
Object[] arr = new Object[this.arr.length + 10]; //arr为局部变量
for (int i = 0; i < this.arr.length; i++) arr[i] = this.arr[i]; //把原来的arr搬过来
this.arr = arr;
}
//把后面的元素往右移动
int i = size - 1; //最后一个元素开始
while (i >= index){ //一直到目标位置
arr[i+1] = arr[i];
i--;
}
arr[index] = e; //插入操作
size ++; //加入后size增长一位
//把数据放进去
}
@Override
public E remove(int index) {
//位置是否合法(添加异常)
if(index > size - 1) throw new IllegalArgumentException("非法的删除位置");
//无需考虑扩容
//前移后面的元素
int i = index;
E e = (E) arr[index]; //用Object存的,因此要进行强制类型转换(一定是安全的)
while(i < size - 1){
arr[i] = arr[i+1]; //前移后面的元素
i++;
}
size--; //减少容量
return e;
}
@Override
public E get(int index) {
//异常处理
if (index >= size ) throw new IndexOutOfBoundsException("无法访问到下标位置");
return (E) arr[index];
}
}
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("A", 0);
list.add("B", 0);
list.remove(0);
System.out.println(list.get(0));
}
}
链表
数据分散的存储在物理空间中,通过一根线保存着它们之间的逻辑关系,这种存储结构称为链式存储结构。
实际上,就是每一个结点存放一个元素和一个指向下一个结点的引用(C语言里面是指针,Java就是对象的引用,代表下一个结点对象)
头节点:不存储数据
public class LinkedList<E> extends AbstractList<E> {
private Node<E> head = new Node<>(null); //头结点
private int size;
@Override
public int size() {
return size;
}
@Override
public void add(E e, int index) {
//位置是否合法(添加异常)
if(index > size) throw new IllegalArgumentException("非法的插入位置");
//链表不需要扩容,因为一直在动态扩容
//插入元素
/**
* e1 -> e2 -> e3
* 插入e0
* e0 -> e2, e1 -> e0即可
*/
//定位前驱节点
Node<E> node = head, temp;
for(int i = 0; i < index; i++){ //根据index数找到存储节点的前驱节点
node = node.next;
}
temp = node.next; //暂时存放
node.next = new Node<>(e);
node.next.next = temp;
size++;
}
@Override
public E remove(int index) { //删除的思路是直接把前驱结点指向后驱
//位置是否合法(添加异常)
if(index > size) throw new IllegalArgumentException("非法的插入位置");
//
Node<E> node = head, temp;
for(int i = 0; i < index; i++){
node = node.next; //由于索引是从0开始,因此这里找到的是前驱结点
}
temp = node.next;
node.next = node.next.next; //前驱结点直接指向后驱结点
size--;
return temp.e;
}
@Override
public E get(int index) {
//位置是否合法(添加异常)
if(index > size) throw new IllegalArgumentException("非法的插入位置");
Node<E> node = head.next;
for(int i = 0 ; i < index; i++){
node = node.next;
}
return node.e;
}
private static class Node<E>{
private E e; //一个元素
private Node<E> next; //一个对象(指针)
public Node(E e){
this.e = e;
}
}
}
public class Main {
public static void main(String[] args) {
AbstractList<String> list = new LinkedList<>();
list.add("A", 0);
list.add("B", 1);
list.add("C", 2);
System.out.println(list.remove(1));
System.out.println(list.get(1));
}
}
比较顺序表和链表的优异
顺序表优缺点:
1、访问速度快,随机访问性能高
2、插入和删除的效率地下,极端情况下需要变更整个表
3、不易扩充,需要复制并重新创建数组
链表优缺点:
1、插入和删除效率高,只需要改变连接点的指向即可。
2、动态扩充容量,无需担心容量问题。
3、访问元素需要依此寻找,随机访问元素效率低下。
链表只能指向后面,能不能指向前面?双向链表!
栈
栈遵循先入后出原则,只能在线性表的一段添加和删除元素。我们可以把栈看作一个杯子,杯子只有一个口进出,最低处的元素只能等到上面的元素离开杯子后,才能离开。
向栈中插入一个元素时,称为入栈,移除栈顶元素称为出栈,我们需要尝试实现以下抽象类型
本次借助线性表完成
public abstract class AbstractStack<E> {
/**
* 出栈操作
* @return 栈顶元素
*/
public abstract E pop();
/**
* 入栈造作
* @param e 元素
*/
public abstract void push(E e);
}
public class ArrayStack<E> extends AbstractStack<E>{
//底层数组
private Object[] arr = new Object[20]; //初始大小为20,后面会动态增长
//长度
private int size = 0; //不仅是长度,也是栈顶指针
@Override
public void push(E e) { //没有index因为位置只能从最上面插入
//如果要扩容,就扩容
if (size >= arr.length){
Object[] arr = new Object[this.arr.length + 10]; //arr为局部变量
for (int i = 0; i < this.arr.length; i++) arr[i] = this.arr[i]; //把原来的arr搬过来
this.arr = arr;
}
arr[size] = e;
size++;
}
@Override
public E pop() {
return (E) arr[(size--)-1]; //都不用去掉原来的数,因为指针变了,下一个进栈直接覆盖原来的数。
}
}
public class Main {
public static void main(String[] args) {
ArrayStack<String> stack = new ArrayStack<>();
stack.push("A");
stack.push("B");
stack.push("C");
System.out.println(stack.pop());
stack.push("D");
System.out.println(stack.pop());
System.out.println("debug");
}
}
其实,我们在JVM在处理方法调用时,也是一个栈操作。
如果用递归不恰当的话,会一直压栈,最后爆栈。
队列
队列同样也是受限制的线性表,不过队列就像我们排队一样,只能从队尾开始排,从队首出。
public abstract class AbstractQueue <E>{
/**
* 进队操作
* @param e 元素
*/
public abstract void offer(E e);
/**
* 出队操作
* @return 元素
*/
public abstract E poll();
}
public class ArrayQueue<E> extends AbstractQueue<E>{
//底层数组
private Object[] arr = new Object[4];
//队尾队首下标
private int head = 0, tail = 0;
@Override
public void offer(E e) {
int next = (tail+1) % arr.length; //防止tail追上head一圈的情况,到了最后把tail置零
if (next == head) return;
arr[tail] = e; //把数扔到队尾对应下标
tail = next; //置数
}
@Override
public E poll() {
E e = (E) arr[head];
head = (head + 1) % arr.length;
return e;
}
}
public class Main {
public static void main(String[] args) {
ArrayQueue<String> queue = new ArrayQueue<>();
queue.offer("A");
queue.offer("B");
queue.offer("C");
queue.offer("D"); //队满了,进不去了
queue.poll();
queue.poll(); //排出两个人
queue.offer("E");
queue.offer("F");
System.out.println("debug");
}
}