(1)链表动态数据结构:需要存储多少数据,就可以生成多少个节点
(2)节点类设计,跟数组不同,数组由一个个数据组成;链表由一个个结点构成;虽然是结点,可以把它看作一种稍复杂的数据;E是真正的数据,next是指向Node的一个引用(Node类型的引用)
(1)链表和数组的底层机制不一样:数组开辟连续分布的空间,可使用索引直接访问,链表的每个节点所在的内存位置是不同的,必须靠next找到元素
链表的实现:与其他线性结构类似,都使用size记录元素个数,需要维护size
节点内部类
//节点类设计,跟数组不同,数组由一个个数据组成;链表由一个个结点构成,
// 虽然是结点,可以把它看作一种稍复杂的数据,内部类
private class Node{
//设置成public,则在LinkedList外面可以随意的修改访问
public E e;
public Node next;
public Node(E e,Node next) {
this.e=e;
this.next = next;
}
public Node(E e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString() {
return e.toString();
}
}
LinkedList的其他成员变量
访问链表中的所有节点,必须把链表头存储起来
private Node head;
private int size;
public LinkedList() {
head=null;
size=0;
}
//获取链表中的元素个数
public int getSize(){
return size;
}
//返回链表是否为空
public boolean isEmpty(){
return size==0;
}
在链表头部添加节点:构造新节点,next指向头节点,修改head指向新节点(注意引用的赋值表示引用指向同一区域)
(1)在数组中添加元素时在尾部最方便,因为size指向数组最后一个元素的下一个位置,即下一个元素待添加的位置,size变量会跟踪数组的尾
(2)在链表头添加元素最方便,head变量跟踪链表头而没有相应的变量跟踪链表的尾
过程在一个函数中执行,当函数结束之后,node节点的作用域就结束了
//为链表头添加元素
public void addFirst(E e){
//创建新的节点
// Node node=new Node(e);
// node.next=head;
// head=node;
//首先实例化Node,直接指向链表的head,再将node赋给head
head=new Node(e,head);
size++;
}
在链表中间添加节点:对链表来说其实没有索引,只做练习,在索引为n的位置添加元素,即在索引为n的结点之前添加结点,head指向的结点索引为0,在头结点添加元素,就是在索引为0的位置添加元素,跟在其他索引位置添加元素的方法不同
关键是要找到要添加的节点的前一个节点,当需要将元素添加在索引为0的位置时,没有前一个节点,需要处理
//在链表的index位置添加新的元素,在链表中很少使用索引,只作练习理解
//搜索新节点要插入位置之前的节点,使用prev遍历
public void add(E e,int index){
if(index<0||index>size){
throw new IllegalArgumentException("add failed");
}
//当需要将元素添加在索引为0的位置时,没有前一个节点
if(index==0)
addFirst(e);
else {
Node prev = head;
for(int i=0;i<index-1;i++){
//把当前prev存的节点的下一个节点放进prev中,prev向前移动
prev=prev.next;
}
// Node node = new Node(e);
// node.next = prev.next;
// prev.next = node;
prev.next=new Node(e,prev.next);
}
size++;
}
//在链表末尾添加元素
public void addLast(E e){
add(e,size);
}
在链表头部添加虚拟头结点:在向链表的任意位置添加元素时,在链表头添加和其他位置添加有区别;在添加元素时要找到待添加位的之前的节点,但链表头没有前面一个节点,所以在逻辑上特殊,为了统一操作,使用链表的虚拟头结点
(1)虚拟头结点不存储任何元素
(2)对链表来说,第一个元素是dummyHead.next对应的结点的元素,dummyHead对用户来说是没有意义的
public LinkedList() {
// head=null;
dummyHead=new Node(null,null);
size=0;
}
(1)head中会存放一个具体的元素,所以初始化时是空的head=null;
(2)引入虚拟结点,dummyHead不应该为空,应该有一个结点dummyHead=new Node(null,null);
(3)此时对一个空的链表来说是存在一个结点的,即虚拟头结点
public void add(E e,int index){
if(index<0||index>size){
throw new IllegalArgumentException("add failed");
}
//当需要将元素添加在索引为0的位置时,没有前一个节点
// if(index==0)
// addFirst(e);
// else {
Node prev = dummyHead;
// for(int i=0;i<index-1;i++){
注意index范围
for(int i=0;i<index;i++){
//把当前prev存的节点的下一个节点放进prev中,prev向前移动
prev=prev.next;
}
// Node node = new Node(e);
// node.next = prev.next;
// prev.next = node;
prev.next=new Node(e,prev.next);
// }
size++;
}
链表的遍历、查询、修改
遍历元素:需要遍历链表中每一个元素,而插入操作的遍历是需要找index的前一个位置相应的结点,从 Node prev = dummyHead开始遍历;而现在的遍历就是要遍历index位置,所以Node cur=dummyHead.next;从索引为0的位置开始(根据目标的不同设定)
//获得链表的第index个位置的元素
public E get(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("get failed");
}
Node cur=dummyHead.next;
for(int i=0;i<index;i++)
cur=cur.next;
return cur.e;
}
修改元素
//修改链表的第index位置的元素
public void set(int index,E e){
if(index<0||index>=size){
throw new IllegalArgumentException("set failed");
}
Node cur=dummyHead.next;
for(int i=0;i<index;i++){
cur=cur.next;
}
cur.e=e;
}
查找元素:没有索引参数了,需要从头进行一次遍历,cur!=null;cur是一个有效结点
//查找链表中是否有元素e
public boolean contains(E e){
Node cur=dummyHead.next;
while(cur!=null){
if(cur.e.equals(e))
return true;
cur=cur.next;
}
//for(Node cur=dummyHead.next;cur!=null;cur=cur.next)
return false;
}
另一种从头遍历的写法:用for循环替代while循环,不涉及size,即不涉及循环多少次,循环条件还是cur!=null
测试
删除元素:删除索引为2的元素,找到待删除元素之前的结点,所以使用prev
跳过2结点,等同于删除了2结点
public E delete(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("delete failed");
}
Node prev=dummyHead;
for(int i=0;i<index;i++){
//把当前prev存的节点的下一个节点放进prev中,prev向前移动
prev=prev.next;
}
Node retNode=prev.next;
prev.next=retNode.next;
retNode.next=null;//注意
size--;
return retNode.e;
}~
链表时间复杂度分析
链表的修改操作需要从头遍历
查找方法需要从头遍历
相比链表,数组有索引可以快速访问,链表没有这种优势,但如果只对链表头进行操作,效率高,所以对链表进行如下操作;对链表来说,与数组类似,有size来记录元素个数,但由于链表元素不是在一片连续空间,即使知道size,也需要遍历一遍链表才能找到最后一个结点,因此链表不适合在链表尾进行操作(不适合需要遍历的操作),而数组适合
起始的dummyHead的next为null,当插入第一个结点时,node.next=prev.next;即为null,接下来不断在链表头插入元素,最后一个结点的next依然为null,因此判断当前结点是否是最后一个,使用next=null判断
由链表适合于在链表头进行操作,而栈是先进后出,只对栈顶进行操作,那么可把链表头当作栈顶,用链表作为栈的底层实现
public class LinkedListStack<E> implements Stack<E>{
private LinkedList<E> linkedList;
public LinkedListStack(){
linkedList=new LinkedList<>();
}
@Override
public void push(E e) {
//父类向子类转型,需要强制转型
linkedList.addFirst(e);
}
@Override
public E pop() {
return linkedList.delete(0);
}
@Override
public E top() {
return linkedList.get(0);
}
@Override
public int getSize() {
return linkedList.getSize();
}
@Override
public boolean isEmpty() {
return linkedList.isEmpty();
}
}
对于数组栈来说,经常需要进行resize动态扩容缩容,而链表不存在这样的情况,链表栈与数组栈的耗时多少跟操作数的多少以及系统有关,链表栈用时不一定比数组栈要快,这种比较是是否复杂的,这是因为链表栈中包含更多的new Node操作,通常操作越多,new操作耗时越明显,数组栈需要进行resize
因此链表栈和数组栈在时间复杂度上没有巨大的差异,而循环队列和数组队列的差异是巨大的
链表实现队列
(1)使用链表实现队列,需要在一端插入元素,在另一端删除元素,在线性结构的两端同时操作,那么用一端操作的复杂度是O(n)级别的,在用数组实现队列时遇到这种问题,采用循环队列改进;因此在用链表实现队列时,也需要进行改进
(2)对链表来说,在头部插入删除元素都是容易的,这是因为有head这个遍历来标记链表的头,同理要是在尾部进行操作方便,创建变量标记尾部tail
(3)在尾部添加元素,相当于在链表索引为size的位置添加元素,tail指向的结点是待添加位置之前的结点,从两端添加节点都很容易
(4)在尾部删除元素,需要找到待删除元素之前的结点,因此仍然需要遍历一遍链表,因此无法使用O(1)复杂度的操作删除tail位置的结点,从tail删除元素不容易
(5)因此在使用链表实现队列时,head端作队首,tail端作队尾,从head端删除元素,从tail端插入元素
(6)由于链表的操作都在链表的一侧完成,不再使用虚拟头结点,这是因为不涉及对链表中间一个元素进行插入或删除,没必要统一对链表中间和两侧元素的操作,没有使用dummyHead
队列为空时,在队尾入队一个结点,注意要同时维护head与tail指向同一个结点, 维护tail tail=node;维护head head=tail;队列出队后为空时,注意要同时维护head和tail,因为经过以上操作,不维护tail,tail指向的依然是res,此时tail应该为空,tail=null;(当列表只有一个元素时,tail=node;head=tail;当列表为空时,head=null;tail=null;)
出队元素时,需要将其与链表断开res.next=null;
public class LinkedListQueue<E> implements Queue<E>{
private class Node{
//设置成public,则在LinkedList外面可以随意的修改访问
public E e;
public Node next;
public Node(E e, Node next) {
this.e=e;
this.next = next;
}
public Node(E e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString() {
return e.toString();
}
}
private Node head;
private Node tail;
private int size;
public LinkedListQueue(){
head=null;
tail=null;
size=0;
}
//返回链表是否为空
public boolean isEmpty(){
return size==0;
}
public E getFront(){
if(isEmpty())
throw new IllegalArgumentException("get failed");
return head.e;
}
public int getSize() {
return size;
}
public String toString() {
StringBuilder res=new StringBuilder();
res.append("head: ");
res.append('[');
Node cur;
for(cur=head;cur!=null;cur=cur.next){
res.append(cur.e+"->");
}
res.append("] tail->null");
return res.toString();
}
@Override
public void enqueue(E n) {
//创建新的节点
Node node=new Node(n);
if(tail==null){
//维护tail
tail=node;
//维护head
head=tail;}
else{
//只需要维护tail
tail.next=node;
tail=node;}
size++;
}
@Override
public E dequeue() {
if(isEmpty())
throw new IllegalArgumentException("remove failed");
Node res= head;
head=head.next;
//经过以上操作,res的next相当于指向了head结点
//需要将res从链表中断开
res.next=null;
//如果列表出队后没有元素了
if(head==null)
//因为经过以上操作,不维护tail,tail指向的依然是res,此时tail应该为空
tail=null;
size--;
return res.e;
}
}
循环队列和链表队列的时间复杂度是一样的