线性表:(List)
-
是n个具有 相同特性 的数据元素的 有限序列 。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串
-
线性表在逻辑上是 线性结构 ,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以 数组 和 链式结构 的形式存储
常用方法列表:(顺序表 和 链表都支持)
返回类型 | 方法 | 解释 |
---|---|---|
boolean | add(E e) | 尾插 |
void | add(int index, E e) | 将 元素e 插到 位置index |
E | remove(int index) | 删除 index位置 元素 |
boolean | remove(Object o) | 删除第一次出现的 指定元素(如果存在) |
int | size() | 返回列表中的元素个数 |
E | get(int index) | 返回 index 下标处的元素 |
E | set(int index, E e) | 用 e 替换列表中下标为 index 位置的元素 |
void | clear() | 清空列表 |
boolean | isEmpty() | 返回列表是否为空 |
boolean | contains(Object o) | 返回列表中是否包含 o 这个对象 |
int | indexOf(Object o) | 返回列表中 o 第一次出现的下标(若不存在,返回 -1) |
int | lastIndexOf(Object o) | 返回列表中 o 最后一次出现的下标(若不存在,返回 -1) |
List < E > | subList(int fromIndex, int toIndex) | 从线性表中截取一段线性表 [fromIndex,toIndex),不影响原线性表 |
void | sort(Comparator cmp) | 对线性表进行排序,以传入的比较器进行元素的比较 |
Iterator< E > | Iterator() | 返回迭代器,进行从前往后的遍历 |
ListIterator< E > | listIterator() | 返回列表迭代器(列表从 0 开始) |
ListIterator< E > | listIterator(int index) | 从 index 开始,返回列表迭代器(列表从 1 开始) |
Object[] | List.toArray() | List 转 数组 |
List | Arrays.asList() | 数组 转 List |
1、顺序表:
(1)神马是顺序表:(ArrayList)
是用一段 物理地址连续 的存储单元依次存储数据元素的 线性结构(逻辑上也连续)
一般采用 数组 存储,也就是在数组上增删查改
① 从 Java语法 的角度:
List 是接口 ; ArrayList 是类,实现了 List
② 从 数据结构 的角度:
List 表示线性表, ArrayList 表示顺序表
通过 ArrayList 实现 List 。。 表达了顺序表是一种线性表
(2)分类:
静态 顺序表: 使用 定长数组 存储
动态 顺序表: 使用 动态开辟的数组 存储
(3)接口的实现:(下面这个为一个简单的顺序表)
ArrayList 相对 List 额外的方法:
方法 | 解释 |
---|---|
ArrayList() | 构造一个初始容量为十的空列表 |
ArrayList(Collection<? extends E> c) | 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。 |
ArrayList(int initialCapacity) | 构造具有指定初始容量的空列表 |
为了更好的对这些方法的理解,手动实现 ArrayList 的一些方法:
github链接:(感兴趣的小伙伴可以看一下)
https://github.com/JACK-QBS/DataStructure/blob/master/%E7%BA%BF%E6%80%A7%E8%A1%A8/MyArrayList.java
- 顺序表中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
为了解决顺序表的弊端,我们引入了单链表:
2、单链表:
(1)神马是单链表 :
是一种 物理存储结构 上 非连续 存储结构,数据元素的 逻辑顺序 是通过链表中的 引用链接次序 实现的 。(逻辑连续)
(2)表现形式:(一共是 8 种)
聪明的你尝试自己组合一下。。。
① 单向 、 双向
② 带头 、 不带头
③ 循环 、 非循环
下来我们简单介绍两种形式的:(面试问的最多!)
链表 由一个一个的 节点组成:
你问我节点是干嘛的?
节点 当然是用来 存储数据 的了。。。
data :数据,不一定是整型的
next :下一个节点的引用(地址)
单向 不带头 非循环链表
(头节点可能会随时改变)
写一个简单的 单向 不带头 非循环链表
/**
* 单链表
*/
class Node {
public int data;
public Node next; // 表示下个节点的引用
public Node(int data) {
this.data = data;
}
}
public class SingleLinkedList {
public static void main(String[] args) {
SingleLinkedList singleLinkedList = new SingleLinkedList();
singleLinkedList.addFirst(6);
singleLinkedList.addLast(1);
singleLinkedList.addFirst(4);
singleLinkedList.addLast(8);
singleLinkedList.addLast(1);
singleLinkedList.addLast(6);
singleLinkedList.display();
//singleLinkedList.remove(1);
//singleLinkedList.display();
System.out.println(singleLinkedList.contains(6));
singleLinkedList.removeAllKey(6);
singleLinkedList.display();
}
public Node head;// 标识头节点
//头插法
public void addFirst(int data) {
Node node = new Node(data);
node.next = this.head;
this.head = node;
}
// 打印
public void display() {
Node cur = this.head;
while(cur != null) {
System.out.print(cur.data+" ");
cur = cur.next;
}
System.out.println();
}
// 尾插法
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;
}
}
// 检查下标是否合法
public boolean checkIndex(int index) {
if(index == 0 || index > this.getLength()) {
System.out.println("下标不合法!");
return false;
}
return true;
}
// 任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data) {
if(!checkIndex(index)) {
return;
}
if(index == 0) {
addFirst(data);
return;
}
if(index == this.getLength()) {
addLast(data);
return;
}
Node cur = searchPrev(index);//cur 此时保存的就是 index-1 位置节点的引用
Node node = new Node(data);
node.next = cur.next;
cur.next = node;
}
public int getLength() {
int count = 0;
Node cur = this.head;
while(cur != null) {
count++;
cur = cur.next;
}
return count;
}
//查找index的位置,找到并返回引用
public Node searchPrev(int index) {
Node cur = this.head;
int count = 0;
while(count < index-1) {
cur = cur.next;
count++;
}
return cur;
}
// 找前驱
public Node searchPrevNode(int key) {
Node cul = this.head;
while (cul.next != null) {
if(cul.next.data == key) {
return cul;
}
cul = cul.next;
}
return null;
}
//删除第一次出现关键字 key 的节点
public void remove(int key) {
// 头节点是要删除的节点
if(this.head == null) return;
if(this.head.data == key) {
this.head = this.head.next;
return;
}
Node cul = searchPrevNode(key);
if(cul == null) {
System.out.println("没有你要删除的数字!");
return;
}
Node del = cul.next; // 所要删除的节点
cul.next = del.next;
}
// 判断是否包含某个元素
public boolean contains(int toFind) {
Node cul = this.head;
while (cul != null) {
if(cul.data == toFind) {
return true;
}
cul = cul.next;
}
return false;
}
// 删除所有值为 key 的节点
public void removeAllKey(int key) {
if(this.head == null) return;
Node prev = this.head;
Node cul = this.head.next;
while(cul != null) {
if (cul.data == key) {
prev.next = cul.next;
cul = cul.next;
} else {
prev = cul;
cul = cul.next;
}
}
if(this.head.data == key) {
this.head = this.head.next;
}
}
}
手动实现 LinkedList,双向带头非循环链表
Github 链接:
https://github.com/JACK-QBS/DataStructure/blob/master/%E7%BA%BF%E6%80%A7%E8%A1%A8/linkedList.java
3、顺序表和链表的区别:(面试)
区别:
顺序表:(物理上和逻辑上都是连续的)
- (1)空间连续、随机访问(时间复杂度是 O(1))(底层是数组)
- (2)中间 或 前面 部分的 插入删除 时间复杂度是 O(n)
- (3)线程不安全
- (4)增容代价大
- (5)顺序表总是要保留一段空间,不使用
链表: (逻辑上连续,物理上不一定连续)
- (1)以结点为单位存储, 随机访问(时间复杂度为 O(n))
- (2)链表 定位到结点 的前提下,插入 / 删除 元素是 O(1),但定位结点往往是 O(n)
- (3)线程不安全
- (4)没有增容问题,插入一个开辟一个空间
- (5)链表的每个结点都要浪费一点空间,保存地址