1.链表的概念
链表是真正的动态数据结构,栈和队列底层只是依托静态数组,靠resize解决固定容量问题
链表是由一个个结点Node构成,数据存储在Node里面,可以把结点看成一个个对象
如上图,Node结点(类-----对象)里面有两个属性 e存储元素内容1、2、3 。 next 是下一个Node结点
2.链表的特点
特点,比起前面的数组,数组是连续空间,链表就不一定是了,没有这个要求,不会浪费空间
3.链表的操作
1.添加
1.从头添加:
2.从中间添加:
1.删除
注意:添加删除的重点是找到对应点的前置结点,想想,要删除头结点呢,解决办法就是给头结点加一个虚拟头结点 置为null
3.具体实现
package com.ffyc.datastructure;
import java.util.Random;
//列表
/*删除的时候加一个虚拟头结点 浪费一个空间 删除操作时间复杂度由O(n)变成O(1)*/
public class LinkedList<T extends Comparable<T>> {
//1.定义节点相关属性
//内部类定义节点
/*节点定义好了 以后向列表中添加一个元素 就是添加一个节点*/
private class Node{
T ele;//节点中的内容
Node next;//索引 作用是指向下一个节点
//构造函数初始化 下次添加元素就是添加一个节点
public Node(T ele){
this.ele=ele;
}
//重写toString 方便后面打印列表而已 重写Ctrl+O
@Override
public String toString() {
return ele.toString();//打印元素内容
}
}
//查看列表中的元素 循环 但不能for循环 不知道多少个节点 定义一个当前节点 用一个索引遍历列表 不断指向下一个 用while循环
public String showLinkedList(){
Node cur=head;//从头开始遍历
StringBuilder sb=new StringBuilder();//把字符串遇到一个加进去遇到一个加进去 最后显示出来 用到缓冲流
while (cur!=null){//当当前结点遍历到null时遍历完 不为空继续遍历
sb.append(cur.ele+"--->");//追加
cur=cur.next;//更新cur 前移
}
return sb.toString();
}
//2.定义链表相关属性
Node head;//链表的头
int size;//链表的长度 即链表中节点的个数
public LinkedList(){//初始化链表
this.head=null;//头为空
this.size=0;
}
//3.添加操作
//3.1在链表头部添加节点
public void addHead(T ele){
/*
//1.先创建节点
Node node=new Node(ele);//传入数据 创建节点
//2.这个节点的next与头结点连接
node.next=head;
//3.更新head头结点
head=node;
//4.更新列表长度
this.size++;
*/
add(0,ele);
}
//在列表的尾部添加
public void addTail(T ele){
add(this.size,ele);
}
//在任意位置添加 需要知道待插入结点的前一个结点pre
public void add(int index,T ele){
// 1.边界处理
//空间是连续的 index不合法就抛异常
if (index<0||index>this.size){
throw new IllegalArgumentException("index is invalid!");
}
//2.创建节点
Node node=new Node(ele);
this.size++;//长度++
Node dummyHead=new Node(null);//虚拟头结点,即头结点的头结点
dummyHead.next=head;//把虚拟头结点放到头结点的前面
/*
//判空 如果链表为空 直接返回
if(head==null){
head=node;
return;
}*/
//如果列表中只有头结点 头结点是没有前一个结点的 单独处理 相当于在头结点添加
/*改进:如果头结点有个虚拟头结点 这样就不用特殊处理了*/
/*if (index==0){
node.next=head;
head=node;//更新头结点
return;
}*/
//3.找到待插入结点的前一个结点
Node pre=dummyHead;//从头找
for (int i=0;i<index;i++){//从1 因为0已经是head了
pre=pre.next;//一个一个往前找 前移
}
//4.插入操作
node.next=pre.next;
pre.next=node;//不能交换位置 不然后半数据丢失
head=dummyHead.next;//就不用判断链表为空 //更新head
}
/*删除操作*/
//头删 删除链表头结点
public T removeHead(){
if (head==null){
return null;//直接返回null 下面操作不能再走
}
T result= head.ele;//保存要删除的元素 以便等会返回
head=head.next;
this.size--;
return result;
}
//尾删 从链表尾结点删除元素 得先遍历 找到尾 需要定义一个当前结点 然后遍历
public T removeTail(){
//判空
if (head==null){
return null;
}
return remove(this.size-1);
}
//删除任意位置的结点
public T remove(int index){
//空间是连续的 index不合法就抛异常
if (index<0||index>=this.size){
throw new IllegalArgumentException("index is invalid!");
}
//定义一个空结点放在头结点的前面 弥补了头结点没有前置结点的缺点 这样所有位置都可以删
Node dummyHead = new Node(null);
dummyHead.next = head;//从头结点的前面开始
// 找到删除结点的前置结点
Node pre = dummyHead;//从最头头开始找
for (int i = 0; i < index; i++) {
pre = pre.next;
}
// 1、拿到删除结点
Node removeNode = pre.next;
T result = (T) removeNode.ele;//删除结点的内容存起来
// 2、删除
pre.next = removeNode.next;
removeNode.next = null;
this.size--;
// 3、重置头结点 为什么重置 因为你删除的是头结点的话头结点就变了
head = dummyHead.next;
return result;
}
//获取链表头结点
public T getHead(){
if (this.size==0){
return null;
}
return this.head.ele;
/*相当于 return this.head==null?null:this.head.ele; */
}
//获取链表中最后一个元素 用一个临时指针遍历
public T getTail(){
if (head==null){
return null;
}
Node cur=head;
for (int i=0;i<this.size-1;i++){
cur=cur.next;
}
return cur.ele;
}
//获取链表中结点的个数
public int getSize(){
return this.size;
}
//判断列表是否为空
public boolean isEmpty(){
return this.size==0;//如果size=0 返回true
}
//判断指定元素是否存在
public boolean contains(T ele){
//说起遍历 就应该想到一个临时的当前结点
Node cur=head;
while (cur!=null){//cur遍历到null就结束了 不等于null就继续遍历
if (cur.ele.compareTo(ele)==0){//让T继承Comparable 就可以用compareTo方法了
return true;
}
cur=cur.next;//不相等相互移 跟下一个比
}
return false;//都比完了还不相等的话 返回false
}
@Override
public String toString() {
return showLinkedList();
}
//根据索引获取指定位置的内容
public T get(int index){
if (index <0||index >= this.size){
throw new IllegalArgumentException("index is invalid!!");
}
if (head==null){
return null;
}
//要遍历就要想到添加临时索引 指向当前结点
Node cur=head;
for (int i=0;i<index;i++){
cur=cur.next;
}
return cur.ele;
}
//修改指定位置的结点内容
public void set(int index,T ele){
if (index<0||index>=this.size){
throw new IllegalArgumentException("index is invalid!!");
}
//要遍历就要想到添加临时索引 指向当前结点
Node cur=head;
for (int i=0;i<index;i++){
cur=cur.next;
}
cur.ele=ele;
}
public static void main(String[] args) {
LinkedList<Integer> linkedList=new LinkedList<>();//创建一个列表 空列表 一个节点都没有
int count=10;
Random random=new Random();
for (int i=0;i<count;i++){
int ele=random.nextInt(100);
linkedList.addHead(ele);
}
System.out.println(linkedList.showLinkedList());
linkedList.add(7,666);
System.out.println(linkedList.showLinkedList());
linkedList.add(0,888);
System.out.println(linkedList.showLinkedList());
//删除索引为0,3,最后一个
linkedList.remove(0);
System.out.println(linkedList.showLinkedList());
linkedList.remove(3);
System.out.println(linkedList.showLinkedList());
linkedList.remove(linkedList.size-1);
System.out.println(linkedList.showLinkedList());
linkedList.removeTail();
System.out.println(linkedList.showLinkedList());
System.out.println("长度"+linkedList.getSize());
System.out.println("头节点"+linkedList.getHead());
System.out.println("尾节点"+linkedList.getTail());
System.out.println("索引为2的结点"+linkedList.get(2));
System.out.println("链表里有没有30"+linkedList.contains(30));
System.out.println("链表是不是为空"+linkedList.isEmpty());
linkedList.set(1,24);
System.out.println(linkedList.showLinkedList());
}
}
测试结果:
4.时间复杂度分析
插入:头插O(1),尾插要遍历到最后面O(n) ,从任意位置就O(n/2)=O(n)
删除:同插入一样
修改:要遍历O(n)
查找:要遍历O(n)