目录
1.链表的概念与结构
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。
【注意】:
- 上图画的链表结构中其中的一种:不带头结点-单向非循环的链表
- 从上图可以看出,链式结构在逻辑上是连续的,但在物理空间上不一定连续
- 现实中的节点一般都是从堆上申请出来的
除了上面这种结构,链表还有很多其他的结构
以下情况组合起来就有8种链表结构:
(1)单向或双向
(2)带有节点或不带头结点
(3)循环或非循环
虽然链表的结构有很多种,但我们使用最多的是下面的这两种
- 不带头单向非循环链表 :结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
- 不带头的双向链表: 在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。
2.链表的实现
(1)定义链表中的节点
👂
class ListNode{
public int value; //数值域
public ListNode next;//存储下一个节点的地址
public ListNode(int value) {
this.value = value;
}
}
(2)定义链表这个类
public class MySingleList {
public ListNode head; //单链表的头节点的引用
//单链表的头节点是相对于整个链表而言的,不能在节点的类中
}
下面的这些方法都是定义在单链表这个类中的
(3)打印链表
💌
public void display(){
ListNode cur=this.head;
while(cur!=null){
System.out.print(cur.value+" ");
cur=cur.next;
}
System.out.println();
}
(4)查找是否包含关键字key是否在单链表当中
👥
public boolean contains(int key){
ListNode cur=this.head;
while (cur!=null){
if(cur.value==key){
return true;
}
cur=cur.next;
}
return false;
}
(5)得到单链表的长度
👮
public int size(){
ListNode cur=this.head;
int size=0;
while (cur!=null){
size++;
cur=cur.next;
}
return size;
}
(6)头插法 (时间复杂度O(1))
在链表头部插入一个新的节点,并让链表的头(head)指向这个新节点
👸
public void addFirst(int data){
ListNode listNode=new ListNode(data);
listNode.next=this.head;
this.head=listNode;
}
【注意】:
listNode.next=this.head;
this.head=listNode;
这两步的顺序不可以颠倒
(7)尾插法 (时间复杂度O(N))
在链表的尾部插入一个新的节点
💦
public void addLast(int data){
ListNode listNode=new ListNode(data);
if(this.head==null){ //判空操作
this.head=listNode; //如果链表为空,直接用head引用要插入的新节点
} else {
ListNode cur=this.head;
while (cur.next!=null){
cur=cur.next; //找到链表的最后一个节点
}
cur.next=listNode; //在尾节点后插入新的节点
}
}
【注意】:
这里while()循环结束的条件是cur.next!=null
因为cur.nxet==null 时,cur指向的是链表的最后一个节点
(8)任意位置插入,指定第一个数据节点为0号下标
💚
//检查index的合法性
private void checkIndex(int index){
if(index<0||index>size()){
throw new IndexOutOfException("Index 不合法");
}
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
checkIndex(index);//判断index的合法性
ListNode listNode=new ListNode(data);
if(index==0){ //index==0,相当于头插
addFirst(data);
return ;
}
if(index==size()){//index==size,相当于尾插
addLast(data);
return ;
}
ListNode cur=this.head;
while ((index-1)!=0){
cur=cur.next;
index--;//让cur指向要插入位置节点的前一个节点
}
listNode.next=cur.next;
cur.next=listNode;
return ;
}
(9)删除第一次出现关键字为key的节点
💢
public void remove(int key){
if(this.head==null){
System.out.println("链表为空!!!!");
return ; //如果链表为空,直接返回
}
if(this.head.value==key){
this.head=this.head.next;
return; //如果第一个节点就是要删除的节点,直接让头节点指向下一个节点
}
ListNode cur=this.head;
while(cur.next!=null){
if(cur.next.value==key){//让cur指向要删除节点的前一个节点
ListNode del=cur.next;
cur.next=del.next;
return ;
}
cur=cur.next;
}
}
【注意】:
如果想要删除头节点需要特殊考虑
3.LinkedList的模拟实现
LinkedList底层就是一个双向链表,在上面我们已经实现了不带头的单链表的各种功能,下面我们来实现一个双向链表
对于双向链表的节点有三个域:数值域、前驱和后继
class ListNode{
public int val;
public ListNode prev; //前驱
public ListNode next; //后继
public ListNode(int val) {
this.val = val;
}
}
在MyLinkList这个类中有两个属性,head和last ,分别用来标记链表的头和尾
public class MyLinkList {
public ListNode head; //标记链表的头
public ListNode last; //标记链表的尾
}
双向链表的功能
(1)打印双向链表
和单链表的打印一样,只用后继域就可以了
public void display(){
ListNode cur=this.head;
while (cur!=null){
System.out.print(cur.val+" ");
cur=cur.next;
}
System.out.println();
}
(2) 头插法
public void addFirst(int data){
ListNode listNode=new ListNode(data);
if(this.head==null){
this.head=listNode;
this.last=listNode;
}else {
listNode.next=this.head;
this.head.prev=listNode;
this.head=listNode;
}
}
(3)尾插法
相比较单链表的尾插,双向链表的尾插要更容易,单向链表进行尾插,需要遍历找到尾巴,而双向链表有指向尾巴的last
public void addLast(int data){
ListNode listNode=new ListNode(data);
if(this.head==null){
this.head=listNode;
this.last=listNode;
}else {
this.last.next=listNode;
listNode.prev=this.last;
this.last=listNode;
}
}
(4)任意位置的插入
public void addIndex(int index,int data){
if(index < 0 || index > size()) {
System.out.println("index不合法!");
return;
}
if(index == 0) {
addFirst(data);
return;
}
if(index == size()) {
addLast(data);
return;
}
//cur拿到了index下标的节点的地址
ListNode cur = searchIndex(index);
ListNode node = new ListNode(data);
node.next = cur;
cur.prev.next = node;
node.prev = cur.prev;
cur.prev = node;
}
private ListNode searchIndex(int index) {
ListNode cur = head;
while (index != 0) {
cur = cur.next;
index--;
}
return cur;
}
(5)判断链表中是否包含某个节点
public boolean contains(int key){
ListNode cur = head;
while (cur != null) {
if(cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
(6)删除第一次出现关键字为key的节点
考虑多种情况:
1.要删除的节点是头节点
2.要删除的节点是尾节点
3.中间节点
4.当链表中只有一个节点并要删除这个节点时
//删除第一次出现关键字为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 {
cur.prev.next=cur.next;
if(cur.next!=null){
cur.next.prev=cur.prev;
}else {
last=last.prev;
}
}
return;
}else{
cur=cur.next;
}
}
}
(7)删除所有值为key的节点
只需对上面的删除第一次出现关键字为key的节点,做一点修改就可,
(8)清空链表
源码:
✨
public void clear(){
ListNode cur=head;
while(cur!=null){
ListNode curNext=cur.next;
cur.prev=null;
cur.next=null;
cur=curNext;
}
this.head=this.last=null;
}
【注意】:
上面的操作方法都是针对于无头的双向链表实现的
如果想实现有头的双向链表,只需在构造方法中,创建一个节点,并让head和last都引用它
public MyLinkList(){
this.head=this.last=new ListNode(-1);
}
对于带头节点的头插法,都要插到头结点的后面
4.LinkedList的使用
4.1LinkedList的基本概念
LinkedList的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。
在集合框架中,LinkedList也实现了List接口,具体如下:
说明:
- LinkedList实现了List接口
- LinkedList的底层使用了双向链表
- LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
- LinkedList的头插、头删、尾插、尾删时效率比较高,时间复杂度为O(1)
- LinkedList的中间位置插入的时间复杂度是O(N)
4.2LinkedList的常用方法
4.2.1 构造方法
public static void main(String[] args) {
LinkedList<Integer> linkedList=new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
LinkedList<Integer> linkedList1=new LinkedList<>(linkedList);
linkedList1.add(888);
System.out.println(linkedList1);
}
4.2.2其他常用方法
public static void main(String[] args) {
LinkedList<Integer> linkedList=new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList.add(6);
linkedList.add(7);
LinkedList<Integer> linkedList1=new LinkedList<>(linkedList);
linkedList1.add(888);
System.out.println(linkedList1);
linkedList.remove(4);
System.out.println(linkedList.get(3));
System.out.println(linkedList.contains(6));
System.out.println(linkedList.indexOf(6));
}
4.2.3LinkedList的遍历
(1) 使用for-each
public static void main(String[] args) {
LinkedList<Integer> linkedList=new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList.add(6);
linkedList.add(7);
for (int x:linkedList) {
System.out.print(x+" ");
}
System.out.println();
(2) 使用迭代器
正序打印
public static void main(String[] args) {
LinkedList<Integer> linkedList=new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList.add(6);
linkedList.add(7);
for (int x:linkedList) {
System.out.print(x+" ");
}
System.out.println();
System.out.println("============");
Iterator<Integer> it=linkedList.iterator();
while (it.hasNext()){
System.out.print(it.next()+" ");
}
System.out.println();
}
倒序打印
ListIterator<Integer> listIterator= linkedList.listIterator(linkedList.size());
while (listIterator.hasPrevious()){
System.out.print(listIterator.previous()+" ");
}
System.out.println();
在倒序打印时,要给定链表的大小
5.ArrayList和LinkedList的区别
- 存储方式
ArrayList是一块连续的内存,在物理上和逻辑上都是连续的
LinkList 物理上不一定连续,逻辑上连续 - 增删查改的区别
增加和删除元素时,建议使用LinkList,只需要修改指向就可以O(1),而ArrayList需要挪动数据O(N)
通过下标进行查找修改元素时,建议使用ArrayList O(1),LinkList是O(N) - ArrayList支持随机访问O(1),LinkList不支持
- ArrayLlis空间不够时需要扩容,LinkList没有容量的概念