Java 原生实现常见数据结构
文章目录
- Java 原生实现常见数据结构
- 一、引言
- 二、数组(Array)
- (一)概念
- (二)代码实现
- 三、链表(Linked List)
- (一)概念
- (二)代码实现(以单链表为例)
- 四、栈(Stack)
- (一)概念
- (二)代码实现(基于数组实现栈)
- 五、队列(Queue)
- (一)概念
- (二)代码实现(基于链表实现队列,也可以基于数组实现)
一、引言
在计算机科学的广袤领域中,数据结构犹如构建高楼大厦的基石,其重要性不言而喻。它们为数据的组织、存储与操作提供了标准化的方式,直接影响着算法的效率以及软件系统的整体性能。Java,作为一门广泛应用于企业级开发、移动应用开发、大数据处理等众多领域的编程语言,虽然其类库已经内置了丰富的数据结构实现,但深入探究如何用 Java 原生代码实现这些数据结构,对于每一位渴望提升编程内功、透彻理解计算机底层原理的开发者来说,都具有不可估量的价值。这不仅能够加深我们对数据结构本质的认识,更能在面对复杂多变的业务需求和性能挑战时,游刃有余地进行定制化开发与优化。本文将带领读者踏上一段深入的编程之旅,详细介绍如何用 Java 原生代码实现几种常见且极为重要的数据结构——数组、链表、栈和队列,并对它们的特性、应用场景以及性能特点进行全面剖析。
二、数组(Array)
(一)概念
数组,作为一种最为基础和常用的线性数据结构,在内存中呈现为一段连续的存储空间。它犹如一列整齐排列的储物柜,每个储物柜都有其唯一的编号(索引),从 0 开始,依次递增。这些储物柜专门用于存放相同类型的数据元素,例如整数、浮点数或者对象引用等。这种连续存储的特性使得数组在随机访问元素时具有极高的效率,只需通过索引,就能在常量时间内迅速定位并获取到指定位置的元素。然而,数组的长度在创建之初就已确定,这就好比储物柜的数量一旦确定就难以轻易更改,当需要插入或删除元素时,尤其是在数组中间位置进行操作时,往往需要移动大量后续元素,这无疑会带来较大的时间开销。
(二)代码实现
public class MyArray {private int[] data; // 存储数组元素的内部数组private int size; // 当前数组中元素的个数// 构造函数,初始化数组的容量public MyArray(int capacity) {data = new int[capacity];size = 0;}// 获取数组中元素的个数public int getSize() {return size;}// 获取数组的容量public int getCapacity() {return data.length;}// 判断数组是否为空public boolean isEmpty() {return size == 0;}// 在数组末尾添加元素public void addLast(int element) {add(size, element);}// 在数组指定位置添加元素public void add(int index, int element) {// 检查索引的合法性,若索引不合法则抛出异常if (index < 0 || index > size) {throw new IllegalArgumentException("索引不合法");}// 当数组已满时,进行扩容操作if (size == data.length) {resize(2 * data.length);}// 将指定位置及之后的元素向后移动一位,为新元素腾出空间for (int i = size - 1; i >= index; i--) {data[i + 1] = data[i];}data[index] = element;size++;}// 获取指定位置的元素public int get(int index) {// 检查索引的合法性,若索引不合法则抛出异常if (index < 0 || index >= size) {throw new IllegalArgumentException("索引不合法");}return data[index];}// 修改指定位置的元素public void set(int index, int newElement) {// 检查索引的合法性,若索引不合法则抛出异常if (index < 0 || index >= size) {throw new IllegalArgumentException("索引不合法");}data[index] = newElement;}// 查找元素是否在数组中存在,返回索引,不存在返回 -1public int contains(int element) {for (int i = 0; i < size; i++) {if (data[i] == element) {return i;}}return -1;}// 删除指定位置的元素,并返回被删除的元素public int remove(int index) {// 检查索引的合法性,若索引不合法则抛出异常if (index < 0 || index >= size) {throw new IllegalArgumentException("索引不合法");}int ret = data[index];// 将指定位置之后的元素向前移动一位,覆盖要删除的元素for (int i = index + 1; i < size; i++) {data[i - 1] = data[i];}size--;// 当数组元素数量过少时,进行缩容操作,以节省内存空间if (size == data.length / 4 && data.length / 2!= 0) {resize(data.length / 2);}return ret;}// 从数组末尾删除元素public int removeLast() {return remove(size - 1);}// 数组扩容或缩容方法private void resize(int newCapacity) {int[] newData = new int[newCapacity];// 将原数组中的元素复制到新数组中for (int i = 0; i < size; i++) {newData[i] = data[i];}data = newData;}
}
可以通过以下方式测试这个自定义数组类:
public class Main {public static void main(String[] args) {MyArray myArray = new MyArray(10);myArray.addLast(1);myArray.addLast(2);myArray.add(1, 3);System.out.println("数组元素个数: " + myArray.getSize());System.out.println("数组容量: " + myArray.getCapacity());System.out.println("索引为 1 的元素: " + myArray.get(1));myArray.remove(1);System.out.println("删除元素后数组元素个数: " + myArray.getSize());}
}
三、链表(Linked List)
(一)概念
链表,同样属于线性数据结构的范畴,但与数组截然不同。它就像是一条由多个节点连接而成的链条,每个节点恰似链条上的一环,包含了数据元素以及指向下一个节点的指针(在单链表的情况下)。链表的节点在内存中并非连续存储,而是通过指针相互链接,这种离散存储的方式使得链表在插入和删除元素时具有独特的优势,只需调整相关节点的指针指向,无需像数组那样大规模地移动元素,从而在频繁进行插入和删除操作的场景中表现出色。然而,链表在随机访问元素方面相对较弱,因为要访问某个特定位置的元素,需要从链表头开始逐个遍历节点,直到找到目标节点,这一过程的时间复杂度为 O(n),其中 n 为链表的长度。
(二)代码实现(以单链表为例)
// 定义链表节点类
class ListNode {int val;ListNode next;ListNode(int val) {this.val = val;this.next = null;}
}// 自定义单链表类
public class MyLinkedList {private ListNode head; // 头节点private int size; // 链表节点个数// 获取链表长度public int getSize() {return size;}// 判断链表是否为空public boolean isEmpty() {return size == 0;}// 在链表头部添加节点public void addFirst(int val) {ListNode newNode = new ListNode(val);newNode.next = head;head = newNode;size++;}// 在链表指定位置添加节点public void add(int index, int val) {// 检查索引的合法性,若索引不合法则抛出异常if (index < 0 || index > size) {throw new IllegalArgumentException("索引不合法");}if (index == 0) {addFirst(val);return;}ListNode prev = head;// 找到要插入位置的前一个节点for (int i = 0; i < index - 1; i++) {prev = prev.next;}ListNode newNode = new ListNode(val);newNode.next = prev.next;prev.next = newNode;size++;}// 在链表末尾添加节点public void addLast(int val) {add(size, val);}// 获取指定位置的节点的值public int get(int index) {// 检查索引的合法性,若索引不合法则抛出异常if (index < 0 || index >= size) {throw new IllegalArgumentException("索引不合法");}ListNode cur = head;// 遍历链表,找到指定位置的节点for (int i = 0; i < index; i++) {cur = cur.next;}return cur.val;}// 修改指定位置节点的值public int set(int index, int newVal) {// 检查索引的合法性,若索引不合法则抛出异常if (index < 0 || index >= size) {throw new IllegalArgumentException("索引不合法");}ListNode cur = head;// 遍历链表,找到指定位置的节点for (int i = 0; i < index; i++) {cur = cur.next;}int oldVal = cur.val;cur.val = newVal;return oldVal;}// 查找元素是否在链表中存在,返回索引,不存在返回 -1public int contains(int val) {ListNode cur = head;for (int i = 0; i < size; i++) {if (cur.val == val) {return i;}cur = cur.next;}return -1;}// 删除指定位置的节点,并返回被删除节点的值public int remove(int index) {// 检查索引的合法性,若索引不合法则抛出异常if (index < 0 || index >= size) {throw new IllegalArgumentException("索引不合法");}if (index == 0) {return removeFirst();}ListNode prev = head;// 找到要删除位置的前一个节点for (int i = 0; i < index - 1; i++) {prev = prev.next;}ListNode retNode = prev.next;prev.next = retNode.next;size--;return retNode.val;}// 从链表头部删除节点,并返回被删除节点的值public int removeFirst() {if (isEmpty()) {throw new IllegalArgumentException("链表为空");}ListNode retNode = head;head = head.next;size--;return retNode.val;}// 从链表末尾删除节点,并返回被删除节点的值public int removeLast() {return remove(size - 1);}
}
以下是测试代码:
public class Main {public static void main(String[] args) {MyLinkedList myLinkedList = new MyLinkedList();myLinkedList.addFirst(1);myLinkedList.addLast(2);myLinkedList.add(1, 3);System.out.println("链表长度: " + myLinkedList.getSize());System.out.println("索引为 1 的节点值: " + myLinkedList.get(1));myLinkedList.remove(1);System.out.println("删除节点后链表长度: " + myLinkedList.getSize());}
}
四、栈(Stack)
(一)概念
栈,作为一种特殊的线性数据结构,遵循后进先出(LIFO)的操作原则。可以将其形象地比喻为一个只能从顶部放入和取出物品的储物箱。栈只提供了两个基本操作:入栈(push),即将元素压入栈顶;出栈(pop),即将栈顶元素弹出。这种特性使得栈在处理具有嵌套结构或需要回溯的问题时大显身手,例如函数调用栈、表达式求值以及括号匹配等场景。当一个函数被调用时,其相关的局部变量、返回地址等信息就会被压入栈中,当函数执行完毕后,这些信息又会按照后进先出的顺序从栈中弹出,从而实现函数调用的正确返回和资源的回收。
(二)代码实现(基于数组实现栈)
public class MyStack {private int[] data; // 存储栈元素的内部数组private int top; // 栈顶指针,指向栈顶元素的下一个位置// 构造函数,初始化栈的容量public MyStack(int capacity) {data = new int[capacity];top = 0;}// 判断栈是否为空public boolean isEmpty() {return top == 0;}// 获取栈中元素的个数public int size() {return top;}// 入栈操作public void push(int element) {// 当栈已满时,进行扩容操作if (top == data.length) {resize(2 * data.length);}data[top++] = element;}// 出栈操作,返回弹出的栈顶元素public int pop() {// 检查栈是否为空,若为空则抛出异常if (isEmpty()) {throw new IllegalArgumentException("栈为空");}int ret = data[top - 1];top--;// 当栈中元素数量过少时,进行缩容操作,以节省内存空间if (top == data.length / 4 && data.length / 2!= 0) {resize(data.length / 2);}return ret;}// 获取栈顶元素,但不弹出public int peek() {// 检查栈是否为空,若为空则抛出异常if (isEmpty()) {throw new IllegalArgumentException("栈为空");}return data[top - 1];}// 栈的扩容或缩容方法private void resize(int newCapacity) {int[] newData = new int[newCapacity];// 将原栈中的元素复制到新栈中for (int i = 0; i < top; i++) {newData[i] = data[i];}data = newData;}
}
测试代码示例:
public class Main {public static void main(String[] args) {MyStack myStack = new MyStack(10);myStack.push(1);myStack.push(2);System.out.println("栈顶元素: " + myStack.peek());System.out.println("弹出元素: " + myStack.pop());System.out.println("栈是否为空: " + myStack.isEmpty());}
}
五、队列(Queue)
(一)概念
队列,与栈类似,也是一种线性数据结构,但它遵循先进先出(FIFO)的原则。就像日常生活中的排队场景,先到达的人先接受服务。队列提供了两个核心操作:入队(enqueue),即在队尾添加元素;出队(dequeue),即从队头取出元素。队列在许多实际应用场景中有着广泛的应用,例如任务调度系统中,任务按照提交的先后顺序依次被处理;消息队列中,消息按照发送的顺序被接收和处理,以确保消息的顺序性和公平性。
(二)代码实现(基于链表实现队列,也可以基于数组实现)
// 定义队列节点类(与链表节点类似)
class QueueNode {int val;QueueNode next;QueueNode(int val) {this.val = val;this.next = null;}
}public class MyQueue {private QueueNode head; // 队头节点private QueueNode tail; // 队尾节点private int size; // 队列中元素的个数// 判断队列是否为空public boolean isEmpty() {return size == 0;}// 获取队列中元素的个数public int getSize() {return size;}// 入队操作,在队尾添加元素public void enqueue(int element) {QueueNode newNode = new QueueNode(element);if (isEmpty()) {head = tail = newNode;} else {tail.next = newNode;tail = newNode;}size++;}// 出队操作,从队头取出元素并返回public int dequeue() {if (isEmpty()) {throw new IllegalArgumentException("队列为空");}int ret = head.val;head = head.next;if (head == null) {tail = null;}size--;return ret;}//