前言
本篇博客主要从Java角度讲解了顺序表和链表的知识,篇幅较长,但如果能够阅读下来,我相信应该会让你在数据结构初阶的认知有一定的提升。
还在持续更新中~~
顺序表
1、什么是线性表?
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见 的线性表:顺序表、链表、栈、队列、串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的 线性表在物理上存储时,通常以数组和链式结构的形式存储 一般数组物理上是连续的,链式结构物理上是不连续的
2、什么是顺序表?
简单来讲,顺序表就是数组。但是又有和数组不同的地方 例如数组有以下特点:
- 不可扩容,初始化多少就只能存多少元素
- 不知道当前数组中有多少有效数据
- 对数组进行操作需要手动实现
- 可以在任意有效位置放置元素
实际上顺序表等于数组加上一定的操作逻辑
2.1 概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
从这里可以看出顺序表在逻辑上和物理上都是连续的
顺序表一般可以分为:
- 静态顺序表:使用定长数组存储。
- 动态顺序表:使用动态开辟的数组进行存储。
静态顺序表适用于明确知道需要存多少数据的场景,例如在刷算法题要使用数组的时候,我们通常会开辟最大长度+10(+10是为了防止越界访问)。
但大多数情况下,数据量都是未知的,使用静态顺序表的定长数组会导致空间开多了浪费,开少了不够用。
相比之下动态顺序表更灵活, 根据需求来动态的分配空间大小
2.2 顺序表实现
这里采用实现一个自己的ArrayList来深刻体会顺序表内部实现逻辑。代码如下,写的比较详细,但如果想真正学会还是要自己动手练几遍。
import java.util.*;
class MyArrayList {
public int[] elem;
public int usedSize;
public MyArrayList() {
this.elem = new int[5];
}
public MyArrayList(int len) {
if (len > 0) {
this.elem = new int[len];
}else {
System.out.println("输入数组长度非法, 自动生成大小为5的数组");
this.elem = new int[5];
}
}
public void add(int pos, int data) {
if (pos < 0 || pos > this.usedSize) {
System.out.println("pos位置不合法, pos=" + pos);
return;
}
if (isFull()) {
this.elem = Arrays.copyOf(this.elem, 2 * this.elem.length);
}
for (int i = this.usedSize; i > pos; i--) {
this.elem[i] = this.elem[i-1];
}
this.elem[pos] = data;
this.usedSize++;
}
public boolean isFull() {
return this.elem.length == this.usedSize;
}
public void display() {
for (int i = 0; i < this.usedSize; i++) {
System.out.print(this.elem[i] + " ");
}
System.out.println();
}
public boolean contains(int find) {
for (int i = 0; i < this.usedSize; i++) {
if (this.elem[i] == find) {
return true;
}
}
return false;
}
public int search(int find) {
for (int i = 0; i < this.usedSize; i++) {
if (this.elem[i] == find) {
return i;
}
}
return -1;
}
public int getPos(int pos) throws Exception {
if (pos < 0 || pos >= this.usedSize) {
throw new Exception("输入位置非法");
}
return this.elem[pos];
}
public int size() {
return this.usedSize;
}
public void setPos(int pos, int value) {
if (pos < 0 || pos >= this.usedSize) {
System.out.println("pos位置不合法, pos=" + pos);
return;
}
this.elem[pos] = value;
}
public void remove(int toRemove) {
int index = this.search(toRemove);
if (index == -1) {
System.out.println("不存在此元素");
return;
}
for (int i = index; i < this.usedSize-1; i++) {
this.elem[i] = this.elem[i+1];
}
this.usedSize--;
}
public void clear() {
this.usedSize = 0;
}
}
当主函数实例化一个MyArrayList时(命名为myArrayList),会出现下面的场景。
myArrayList这个引用会被放到栈里面。所有实例化的元素都会被存放到堆中,所以myArrayList会指向堆中的一块区域,这块区域存放的就是被new出来的MyArrayList。new MyArrayList时构造方法会自动new一个数组,这个被new出来的数组也会在堆中开辟一块空间(注意不是在MyArrayList这块区域),elem会指向这块区域。
2.3 顺序表的问题及思考
优点
- 可以通过下标快速查找到指定元素,时间复杂度为O(1)
- 空间利用率较高,所有的空间开销都用来存放数据
缺点
- 顺序表中间/头部的插入删除,时间复杂度为O(N),时间复杂度较大
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的资源开销。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,假如我们只想再插入5个数据,后面不再插入了,那么就浪费了95个数据空间。
思考: 如何解决以上问题呢?不妨先学习一下链表的知识,看看能否从中找到答案。
链表
1、链表的概念及结构
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。 链表逻辑上连续,物理上不一定连续
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
- 单向、双向
- 带头、不带头
- 循环、非循环
在这里主要讲解单向不带头节点非循环和双向不带头节点非循环
单向不带头节点非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
双向不带头非循环链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。 为了大家能够更好的理解这几种结构,以下就画图来尽可能的展示一下,希望能对大家有所帮助。
2、链表实现
class ListNode {
public int val;
public ListNode next;
public ListNode() {
}
public ListNode(int val) {
this.val = val;
}
}
public class MyLinkedList {
ListNode head = null;
public static void main(String[] args) {
MyLinkedList list = new MyLinkedList();
list.createList();
list.display();
}
public void createList() {
ListNode listNode1 = new ListNode(12);
ListNode listNode2 = new ListNode(45);
ListNode listNode3 = new ListNode(6);
ListNode listNode4 = new ListNode(8);
ListNode listNode5 = new ListNode(5);
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
listNode4.next = listNode5;
head = listNode1;
}
public void display() {
ListNode tmp = this.head;
while (tmp != null) {
System.out.println(tmp.val);
tmp = tmp.next;
}
}
public void addFirst(int data){
ListNode tmp = new ListNode(data);
tmp.next = this.head;
this.head = tmp;
}
public void addLast(int data) {
ListNode tmp = new ListNode(data);
if (this.head == null) {
head = tmp;
return;
}
ListNode cur = this.head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = tmp;
}
public boolean addIndex(int index,int data) {
int size = this.size();
if (index < 0 || index > size) {
return false;
}
if (index == 0) {
addFirst(data);
return true;
}
if (index == size) {
addLast(data);
}
ListNode tmp = new ListNode(data);
ListNode prev = this.head;
int count = 0;
while (count != index-1) {
prev = prev.next;
count++;
}
tmp.next = prev.next;
prev.next = tmp;
return true;
}
public boolean contains(int key) {
ListNode tmp = this.head;
while (tmp != null){
if (tmp.val == key) {
return true;
}
tmp = tmp.next;
}
return false;
}
public int size() {
ListNode tmp = this.head;
int count = 0;
while (tmp != null) {
count++;
tmp = tmp.next;
}
return count;
}
public void clear() {
ListNode cur = this.head;
while (cur != null) {
ListNode curNext = cur.next;
cur.next = null;
cur = curNext;
}
head = null;
}
}
总结
提示:这里对文章进行总结:
|