概述
前面我们讲的线性表的顺序存储结构,它是有缺点的, 最大的缺点就是插入和删 除时需要移动大量元素,这显然就需要耗费时间,那我们能不能想办法解决呢?
要解决这个问题,我们就得考虑一下导 致这个问题的原因。
为什么当插入和删除时,就要移动大量元素,仔细分析后,发现原因就在于相邻 两元素的存储位置也具有邻居关系。它们编号是1,2,3,...,n , 它们在内存中的位 置也是挨着的,中间没有空隙当然就无法快速介入,而 删除后,当中就会留 出空隙,自然需要弥补 。问 题就出在这 里。
A同学思路:让当中每个元素之间都留有一个空位置,这样要插入时,就不至于移动。可一个空位置如何解决多个相同位置插入数据的问题呢?所以这个想法显然不 行。
B 同学思路: 那就让当中每个元素之间都留足够多的位置,根据实际情况制定空 隙大小,比如 10 个,这样插入时,就不需要移动了。万一 10 个空位用完了,再考虑 移动使得每个位置之间都有 10 空位置。 如果删除,就直接删掉,把位置留空即 可。这样似乎暂时解快了插入和删除的移动数据问题。可这对于超过 同位置数据的插入,效率上还是存在问题。对于数据的遍历,也会因为空位置大多而造成判断 时间上的液费。而且显然这里空间复杂度还增加了,因为每个元素之间都有若干个空 位置。
C同学思路: 我们反正也是要让相邻元素间留有足够余地,那干脆所有的元素都不要考虑相邻位置了,哪有空位就到哪里,而只是让每个元素知道它下个元素的位置在哪里,这样,我们可以在第一个元素时,就知道第二个元素的位置(内存地址) , 而找到它;在第二个元素时,再找到第三个元素的位置(内存地址)。这样所有的元素我们就都可以通过遍历而找到。
很明显C同学这个想法非常好!我们要的就是这个思路。 因此: java 集合中又引入了一个 LinkedList ,即链表结构。
1 链表
1.1 概念
那么什么是链表呢?链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。
为了表示 数据ai与其直 接后继数据元素 ai+1之 间的逻辑关系,对数据元素ai来说, 除了存储 其本 身的信息之外,还需存储一 个指示 其 直接后继 的信息(即直接后继的存储位置 )。我们 把存储数据 元素信息 的域称为 数据域 , 把存储直接后继位置的域称为 指针域 。指针域中存储的信息称做指针或链。这两部分信息组 成数据元素ai的存储映像,称为 (No de)。
对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中 第一个结点的存储位置叫做 头指针 ,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个节点,其实就是上一个后继指针指向的位置想象一下,最后一个节点,它的指针指向哪里呢?
最后一个, 当然就意味着直接后继不存在了,所以我们规定,线性链表的最后一个结点指针为"空"(通常用 NULL或"^"符号表示 )。
有时,我们为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,谁叫它是第一个呢,有这个特权。也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个节点的指针。
1.2 头结点与头指针的异同
实际中链表的结构非常多样:
1.单向或者双向
2.带头或者不带头
3.循环或者非循环
2 链表的实现
2.1 节点的创建
链表是由一个个节点所组成的,后一个结点依靠前一个才能找到,而节点是由数据域(value),以及指针域(next)组成,对于数据域,其是引用类型,存放下一个节点的地址,同时设置构造函数对val进行初始化。
public class MySingleLinkedList {
class ListNode{
public int val;
public ListNode next;
public ListNode(int val, ListNode next) {
this.val = val;
next = null;
}
}
public ListNode head;//代表链表的头结点
}
2.2 创建链表
创建完节点后,我们就要来创建一个链表
public void createList() {
ListNode node1 = new ListNode(12);
ListNode node2 = new ListNode(23);
ListNode node3 = new ListNode(34);
ListNode node4 = new ListNode(45);
ListNode node5 = new ListNode(56);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
this.head = node1;
}
2.3 链表的操作
对于一个创建好节点的链表,它的操作有以下这些:
//头插法
public void addFirst(int data){
}
//尾插法
public void addLast(int data){
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
}
//删除所有值为key的节点
public void removeAllKey(int key){
}
//得到单链表的长度
public int size(){
return -1;
}
public void clear() {
}
public void display() {}
2.4 链表的插入
链表的插入有三种,头插法,尾插法和指定位置插入。
1.头插法
链表的头插法(Head Insertion)是指在链表的头部添加新节点的操作。在单链表中,这种操作相对简单,因为不需要改变已有节点的指针,只需要创建一个新的节点,并将其链接到当前链表的头部。
-
创建新节点:为新节点分配内存,并设置它的数据域(通常是新插入的数据)和前驱指针(指向当前链表的头节点)。
-
更新头指针:将新节点的后继指针设置为原来的头节点,这样新节点就成为了新的链表头。
-
如果原链表为空(即头指针是 或者指向空),则新节点同时是头节点和尾节点。
//头插法
public void addFirst(int val) {
Node node = new Node(val);
//判断链表是否为空
if(size == 0){
this.head = node;
}else {
//链表不为空
node.next = head;
head = node;
}
//元素个数+1
this.size++;
}
2.尾差法
链表的尾插法是一种在链表的末尾添加新节点的方法,它通常用于动态数据结构中,因为不需要像数组那样预先知道链表的长度。
-
创建新节点:首先,你需要创建一个新的节点,包含你要插入的数据。
-
获取尾指针:找到当前链表的最后一个节点。如果链表为空,新节点就是第一个节点(头节点)。
-
更新指针:将新节点的指针指向当前的尾节点,表示新节点是下一个元素。
next
-
设置尾节点:如果当前节点就是链表的最后一个节点,那么它的指针也需要更新为新节点,这样新节点就成为了新的尾节点。
public void addLast(int val) {
ListNode node = new ListNode(val);
if(head == null) {
head = node;
return;
}
ListNode cur = head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
3.任意位置插入
现在,我们要在index位置插入一个整形的value,我们要如何操作呢?既然是在index的位置插入,那么我们就需要知道这个位置前一个位置的信息,所以我们要把cur走index-1步,即在第3位置插入我们的cur就要走2步。
public void addIndex(int index,int val) {
//1.判断index的合法性
try {
checkIndex(index);
}catch (IndexNotLegalException e) {
e.printStackTrace();
}
//2.index == 0 || index == size()
if(index == 0) {
addFirst(val);
return;
}
if(index == size()) {
addLast(val);
return;
}
//3. 找到index的前一个位置
ListNode cur = findIndexSubOne(index);
//4. 进行连接
ListNode node = new ListNode(val);
node.next = cur.next;
cur.next = node;
}
private ListNode findIndexSubOne(int index) {
int count = 0;
ListNode cur = head;
while (count != index-1) {
cur = cur.next;
count++;
}
return cur;
}
private void checkIndex(int index) throws IndexNotLegalException{
if(index < 0 || index > size()) {
throw new IndexNotLegalException("index不合法");
}
}
2.5 查找是否包含关键字key是否在单链表当中
怎么查找呢?我们只要对链表进行遍历即可
public boolean contains(int val) {
ListNode cur = head;
while (cur != null) {
if(cur.val == val) {
return true;
}
cur = cur.next;
}
return false;
}
2.6 链表的删除
1 删除第一次出现关键字为key的节点
现在我们有这么一个链表
我们要删除其中的34这个值,我们应该如何操作?
这个问题的关键所在就是找到34的前一个value,并且删除当前34位置的值。
public void remove(int val) {
//判断是否为空
if(head == null) {
return;
}
if(head.val == val) {
head = head.next;
return;
}
ListNode cur = head;
while (cur.next != null) {
if(cur.next.val == val) {
ListNode del = cur.next;
cur.next = del.next;
return;
}
cur = cur.next;
}
}
2 删除所有值为key的节点
与删除一个元素不同的是,删除所有值为key的节点,在循环判断时找到指定元素时不退出,继续进行查找,直到链表遍历完成.
public void removeAllKey(int val) {
//1. 判空
if(this.head == null) {
return;
}
//2. 定义prev 和 cur
ListNode prev = head;
ListNode cur = head.next;
//3.开始判断并且删除
while(cur != null) {
if(cur.val == val) {
prev.next = cur.next;
//cur = cur.next;
}else {
prev = cur;//prev = prev.next;
//cur = cur.next;
}
cur = cur.next;
}
//4.处理头节点
if(head.val == val) {
head = head.next;
}
}
3 清空链表
要想清空一个链表,我们直接head = null其实就行了,不过我们也可以对每个节点都置空。
public void clear() {
//head = null;
ListNode cur = head;
while (cur != null) {
ListNode curN = cur.next;
//cur.val = null;
cur.next = null;
cur = curN;
}
head = null;
}
2.7 链表的翻转
public ListNode reverseList() {
if(head == null) {
return head;
}
ListNode cur = head.next;
head.next = null;
while(cur != null) {
ListNode curN = cur.next;
//开始翻转
cur.next = head;
head = cur;
cur = curN;
}
return head;
}
2.8 无头双向链表的实现
public class MyLinkedList {
static class ListNode {
public int val;
public ListNode prev;//前驱
public ListNode next;//后继
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;//标志头节点
public ListNode last;//标志尾结点
//得到双向链表的长度
public int size(){
int count = 0;
ListNode cur = head;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
public void display(){
ListNode cur = head;
while (cur != null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
ListNode cur = head;
while (cur != null) {
if(cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
//头插法
public void addFirst(int data){
ListNode node = new ListNode(data);
if(head == null) {
//是不是第一次插入节点
head = last = node;
}else {
node.next = head;
head.prev = node;
head = node;
}
}
//尾插法
public void addLast(int data){
ListNode node = new ListNode(data);
if(head == null) {
//是不是第一次插入节点
head = last = node;
}else {
last.next = node;
node.prev = last;
last = last.next;
}
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
try {
checkIndex(index);
}catch (IndexNotLegalException e) {
e.printStackTrace();
}
if(index == 0) {
addFirst(data);
return;
}
if(index == size()) {
addLast(data);
return;
}
//1. 找到index位置
ListNode cur = findIndex(index);
ListNode node = new ListNode(data);
//2、开始绑定节点
node.next = cur;
cur.prev.next = node;
node.prev = cur.prev;
cur.prev = node;
}
private ListNode findIndex(int index) {
ListNode cur = head;
while (index != 0) {
cur = cur.next;
index--;
}
return cur;
}
private void checkIndex(int index) {
if(index < 0 || index > size()) {
throw new IndexNotLegalException("双向链表插入index位置不合法: "+index);
}
}
//删除第一次出现关键字为key的节点
public void remove(int key){
ListNode cur = head;
while (cur != null) {
if(cur.val == key) {
//开始删除 处理头节点
if(cur == head) {
head = head.next;
if(head != null) {
head.prev = null;
}else {
//head == null 证明只有1个节点
last = null;
}
}else {
cur.prev.next = cur.next;
if(cur.next == null) {
//处理尾巴节点
last = last.prev;
}else {
cur.next.prev = cur.prev;
}
}
return;//删完一个就走
}
cur = cur.next;
}
}
//删除所有值为key的节点
public void removeAllKey(int key){
ListNode cur = head;
while (cur != null) {
if(cur.val == key) {
//开始删除 处理头节点
if(cur == head) {
head = head.next;
if(head != null) {
head.prev = null;
}else {
//head == null 证明只有1个节点
last = null;
}
}else {
cur.prev.next = cur.next;
if(cur.next == null) {
//处理尾巴节点
last = last.prev;
}else {
cur.next.prev = cur.prev;
}
}
//return;//删完一个就走
}
cur = cur.next;
}
}
//清空链表
public void clear(){
ListNode cur = head;
while (cur != null) {
ListNode curN = cur.next;
//cur.val = null;
cur.prev = null;
cur.next = null;
cur = curN;
}
head = last = null;
}
}
3 LinkedList
3.1 什么是LinkedList
LinkedList 的底层是 双向链表结构 ,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。
在集合框架中,LinkedList也实现了List接口:
3.2 LinkedList的使用
public static void main(String[] args) {
//无参构造
List<Integer> list1 = new LinkedList<>();
//有参构造
List<String> list2 = new ArrayList<>();
list2.add("JavaSE");
list2.add("JavaWeb");
list2.add("JavaEE");
// 使用ArrayList构造LinkedList
List<String> list3 = new LinkedList<>(list2);
}
4 ArrayList和LinkedList的区别