引
顺序表和链表是最基本的数据结构, 也是最简单的数据结构, 接下里就带大家简单了解下一顺序表和链表~
基本概念
顺序表
顺序表:
顺序表使用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。顺序表其实就是一个数组,只是它写在类里的它与数组不一样,写到类里面将来就可以面向对象了
- 顺序表也是线性表的一种, 常见的线性表:顺序表,链表,栈,队列,字符串, 线性表在逻辑上是线性结构,在物理上存储时通常以数组和链式结构的形式存储
链表
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的, 是一种链式结构。
下图是一个单链表的节点, 其中val域存储的是当前节点的值, next域存储的是指向下一个节点的地址即引用类型
下图是一个单链表
从上图可以看出:
- 链式存储在逻辑上存储是连续的, 但物理上(即内存上)不一定连续
- 因为现实中的节点一般都是从堆上申请出来的
- 因为是从堆上申请的所以两次申请的空间可能连续也可能不连续
链表的分类
链表有:单向,双向,带头,不带头,循环
分别代表的意思是:
- 单向: 直指向后继
- 双向: 可指向前驱也可指向后继
- 带头: 意思是指会有一个引用一直指向头节点(名为head), 并且它的指向一直都不会变, 即是一个傀儡节点
- 不带头: 也是会有一个引用指向头节点(名为head), 但它的指向是可以发生改变的
- 循环: 链表的最后一个节点的next域存储的是头节点的地址, 此时的链表就是一个环状的了也就是循环
非循环可排列组合为:八种结构
- 带头循环单向
- 带头,非循环单向
- 不带头,循环单向
- 不带头,非循环双向
- 带头,循环双向
- 带头,非循环双向
- 不带头,循环双向
- 不带头,非循环
其中最重要的是: 单向,不带头,非循环 和 双向,不带头,非循环
线性结构和链式结构
为什么说顺序表是线性结构, 链表是链式结构? 线性结构和链式结构又是什么?
因为顺序表的底层是一个数组, 链表是一个由若干个节点组成的一种数据结构, 由于去节点存储的是引用, 因此可以跳转的指向其他地方
其实数据结构大体上可以分为两大类: 一个是顺序式, 一个是跳转式即链式结构
线性结构:
- 在内存上开辟的是一块连续的存储空间
- 需要开辟一个一定长度的空间
- 在物理上连续
链式结构:
- 在内存上开辟的是一块非连续的存储空间
- 不需要开辟一定的空间, 什么时候存储什么时候开辟
- 在逻辑上连续
区别
顺序表和链表的区别
我们可以从顺序表和链表的组织和操作这两个方面来看到其二者区别
组织上:
- 顺序表底层是一个数组,它是一个逻辑上和物理上都是连续的
- 链表是一个由若干个节点组成的一个数据结构,逻辑上是连续的,但是在物理上(即内存上)不一定是连续的。
操作上:
- 顺序表,适合查找相关的操作,因为可以使用下标,直接获取到某个位置的元素。
- 链表,适用于频繁的插入和删除操作。此时不需要像顺序表一样,移动元素。链表的插入只需要修改指向就好了但插入指定位置还得找!
- 顺序表还有不好的地方,就是你需要看慢不慢,满了要扩容,扩容之后不一定都能放满,所以他的(空间上的)利用率不高
数组和顺序表的区别?
- 首先从类型上看, 顺序表是一个具体的实现类, 它可以有自己的方法, 而数组却不是, 也就是说数组可以做到的事情顺序表也可以做到, 而顺序表可以做到的数组并不一定能做到
- 其次, 顺序表能够表示有效的数据个数, 而数组不行
ArrayList和LinkedList的区别
ArrayList和LinkedList是集合框架当中的两个类集合框架就是将 所有的数据结构,封装成了java自己的类。以后我们要用到顺序表了或者链表就可以直接使用ArrayList和LinkedList就可以了
| 不同点 | ArrayList | LinkedList |
|---|---|---|
| 存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
| 随机访问 | 支持O(1) | 不支持:O(N) |
| 头插 | 需要搬移元素,效率低O(N) | 只需修改引用的指向,时间复杂度为O(1) |
| 插入 | 空间不够时需要扩容 | 没有容量的概念 |
| 应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
ArrayList
ArrayList
- ArrayList实现了RandomAccess接口,表明ArrayList支持随机访问
- ArrayList实现了Cloneable接口,表明ArrayList是可以clone的
- ArrayList实现了Serializable接口,表明ArrayList是支持序列化的
- 和Vector不同,ArrayList不是线程安全的,在单线程下可以使用,在多线程中可以选择Vector或者 CopyOnWriteArrayList
- ArrayList底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表
LinkedList
LinkedList
LinkedList的底层是双向链表结构(链表后面介绍),由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高
- LinkedList实现了List接口
- LinkedList的底层使用了双向链表
- LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
- LinkedList的任意位置插入和删除元素时效率比较高,时间复杂度为O(1)
模拟实现
顺序表的重要方法
- 增:
void add(int pos, int date在pos位置新增元素 - 删:
void remove(int toRemove)删除第一次出现的关键字key - 查:
int getPos(int pos)获取pos位置的元素 - 改:
void setPos(int pos, int value)给pos位置的元素设置位value
链表的重要方法
-
增:
void addFirst(int data)头插法, 插在头结点的前面void addLast(int data)尾插法, 插在尾巴节点的后面 -
删:
void remove(int key)删除第一次出现的关键字为key的节点 -
查:
int get(int index)找到index下标位置的节点 -
改:
int set(int index, int element)index下标的val设为element
顺序表的模拟实现
主要实现以下几个方法:
- 打印顺序表
- 获取顺序表长度
- 在pos位置新增元素
- 判断是否包含某个元素
- 查找某个元素的对应位置
- 获取pos位置的元素
- 删除第一次出现的关键字key
- 清空顺序表
代码如下:
public class MyArrayList {
public int[] elem;
public int usedSize;//表示有效的数据个数
public MyArrayList() {
this.elem = new int[10];//在构造方法这里初始化数组
}
//1.打印顺序表
public void display() {
for (int i = 0; i < usedSize; i++) {
System.out.print(this.elem[i]+" ");
}
System.out.println();
}
//2.获取顺序表的有效数据长度
public int size () {
return this.usedSize;
}
//3.在pos位置新增元素
public void add(int pos, int date) {
if (pos < 0 || pos > usedSize) {
System.out.println("pos位置不合法");
return;
}
if (isFill()) {
this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
}
for (int i = this.usedSize - 1; i > pos; i--) {
this.elem[i+1] = this.elem[i];
}
this.elem[pos] = date;
this.usedSize++;
}
//判断是否满了
public boolean isFill() {
return this.usedSize == this.elem.length;
}
//判断是否包含某个元素
public boolean contains(int toFind) {
for (int i = 0; i < this.usedSize; i++) {
if (this.elem[i] == toFind) {
return true;
}
}
return false;
}
//查找某个元素对应的位置
public int search(int toFind) {
for (int i = 0; i < this.usedSize; i++) {
if(this.elem[i] == toFind) {
return i;
}
}
return -1;
}
//获取pos位置的元素
public int getPos(int pos) {
if (pos < 0 || pos > this.usedSize) {
System.out.println("pos位置不合法:>");
return -1;
}
if (isEmpty()) {
System.out.println("顺序表为空");
return -1;
}
return this.elem[pos];
}
public boolean isEmpty() {
return this.usedSize == 0;
}
//给pos位置的元素设置位value
public void setPos(int pos, int value) {
if (pos < 0 || pos > this.usedSize) {
System.out.println("pos位置不合法:>");
return;
}
this.elem[pos] = value;
}
//删除第一次出现的关键字key
public void remove(int toRemove) {
int index = 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 clean() {
this.usedSize = 0;
}
}
单向不带头链表的模拟实现
主要实现以下几个方法:
- 打印链表
- 得到单链表的长度
- 判断是否包含某个元素
- 头插和尾插
- 删除第一次出现的关键字key
- 清空顺序表
代码如下:
//实现单向链表(不带傀儡节点)代码
public class SingleLinkedList {
Node head;//头节点
public SingleLinkedList() {
}
//头插法
public void addFirst(int data) {
Node node = new Node(data);
if (this.head != null) {
node.next = this.head;
}
this.head = node;
}
//尾插法
public void addLast(int data) {
Node node = new Node(data);
if (this.head == null) {
this.head = node;
} else {
Node cur = this.head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
}
//任意位置插入,第一个数据节点为0号下标
public boolean addIndex(int index,int data) {
if (index == 0) {
addFirst(data);
return true;
}
if (index == this.size()) {
addLast(data);
return true;
}
Node node = new Node(data);
Node cur = checkIndex(index);
node.next = cur.next;
cur.next = node;
return true;
}
private Node checkIndex(int index) {
if (index < 0 || index > this.size())
throw new IndexOutOfBoundsException("index is illegal");
Node cur = this.head;
while (index - 1 != 0) {
cur = cur.next;
index--;
}
return cur;
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key) {
Node cur = this.head;
while (cur != null) {
if (cur.val == key)
return true;
cur = cur.next;
}
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
if (this.head.val == key) this.head = this.head.next;
Node cur = this.head;
while (cur.next != null) {
if (cur.next.val == key) {
cur.next = cur.next.next;
return;
}
cur = cur.next;
}
System.out.println("删除失败!");
}
//删除所有值为key的节点
public void removeAllKey(int key) {
while (this.head.val == key) this.head = this.head.next;
Node prev = this.head;
Node cur = this.head.next;
while (cur != null) {
if (cur.val == key)
prev.next = cur.next;
else
prev = cur;
cur = cur.next;
}
}
//得到单链表的长度
public int size() {
Node cur = this.head;
int count = 0;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
//打印链表
public void display() throws NullPointerException {
if (this.head == null) {
throw new NullPointerException("空指针访问异常");
}
Node cur = this.head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
//任意位置打印
public void printList1(Node listNode) throws NullPointerException {
if (this.head == null) {
throw new NullPointerException("空指针访问异常");
}
Node cur = listNode;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
public void clear() {
while (this.head != null){
Node headNext = this.head.next;
this.head.next = null;
this.head = headNext;
}
}
}
无头双向链表的模拟实现
直接上代码~
// 无头双向链表实现
public class MyLinkedList {
public ListNode head;
public ListNode last;
//头插法
public void addFirst(int data) {
ListNode node = new ListNode(data);
if (this.head == null) {
this.head = node;
this.last = node;
} else {
node.next = this.head;
this.head.prev = node;
this.head = node;
}
}
//尾插法
public void addLast(int data) {
ListNode node = new ListNode(data);
if (head == null) {
this.head = node;
this.last = node;
} else {
this.last.next = node;
node.prev = last;
this.last = node;
}
}
//任意位置插入,第一个数据节点为0号下标
public boolean addIndex(int index,int data) {
if (index < 0 || index > size(head)) {
return false;
}
if (index == 0) {
addFirst(data);
return true;
}
if (index == size(head) - 1) {
addLast(data);
return true;
}
ListNode node = new ListNode(data);
ListNode curNode = findIndex(index);
node.next = curNode.next;
curNode.next = node;
curNode.next.prev = node;
node.prev = curNode;
return true;
}
private ListNode findIndex(int index) {
while (index - 1 != 0) {
this.head = this.head.next;
index--;
}
return this.head;
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key) {
ListNode cur = this.head;
while (cur != null) {
if (cur.val == key) return true;
cur = cur.next;
}
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key) {
if (isEmpty()) return;
if (key == this.head.val) {
this.head = this.head.next;
return;
}
if (key == this.last.val) {
ListNode prevNode = this.last.prev;
this.last.prev = null;
this.last = prevNode;
this.last.next = null;
return;
}
ListNode cur = this.head;
while (cur != null) {
if (cur.val == key) {
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
return;
} else
cur = cur.next;
}
}
private boolean isEmpty() {
return size(head) == 0;
}
//删除所有值为key的节点
public void removeAllKey(int key) {
if (isEmpty()) return;
while (this.head != null && this.head.val == key) {
this.head = this.head.next;
}
while (this.last.val == key) {
this.last = last.prev;
this.last.next = null;
}
ListNode cur = this.head;
while (cur != null) {
if (cur.val == key) {
ListNode curNext = cur.next;
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
cur = curNext;
} else
cur = cur.next;
}
}
//得到单链表的长度
public int size(ListNode head) {
if (head == null) {
return 0;
}
return size(head.next) + 1;
}
public void display() {
if (this.head == null) {
System.out.println("链表为空!");
return;
}
ListNode cur = this.head;
while (cur != null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
public void clear() {
while (this.head != null) {
ListNode nextNode = this.head.next;
this.head.prev = null;
this.head.next = null;
this.head = nextNode;
}
}
}
总结
顺序表
缺点:
- 中间或前面部分的插入删除时间复杂度O(N)
- 增容的代价比较大,会造成内存空间的浪费
优点:
空间连续,支持随机访问
链表
缺点:
- 以节点为单位存储, 访问速度相对于顺序表会低一些
- 不支持随机访问
优点:
- 任意位置插入删除时间复杂度为O(0)
- 没有增容问题,插入一个开辟一空间 链表随用随取,new一个