课程《玩转数据结构》学习java
链表是典型的线性动态数据结构,也是学习树形数据结构的敲门砖。与数组不一样,链表的意义在于动态二字。再回顾一下什么是数组:在内存中开辟一段连续的存储空间的相同数据类型元素存储的集合 。数组并不具有动态的能力,为了让数组具备动态的特性,咱们能够实现本身的数组,让其具有自动扩容以及缩容(resize)的能力。动态数组。
而对于栈,与队列这两种具有特殊功能的线性数据结构,可使用数组做为底层原理来实现。对于栈的特性即LIFO,使用动态数组做为底层实现知足了栈各个功能的时间复杂度为O(1)。而队列的特性为:FIFO,若是使用数组做为底层,在队列的出队操做时,这一项功能的时间复杂度就为O(n)。使用循环队列的思想,则能够将出队操做优化至O(1)。
链表则是一种真正的动态数据结构。由于数组在内存的空间是连续的,因此最大的优点 是支持“随机访问”,而链表最大的优势则是“真正的动态”。链表不会浪费多余的内存空间,不须要处理容量的问题,可是也丧失了数组的随机访问的能力。
node
public class LinkedList<E>{
private class Node{
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 int size;
public LinkedList(){
head = null;
size = 0;
}
// 获取链表中元素的个数
public int getSize(){
return size;
}
// 判断链表是否为空
public boolean isEmpty(){
return size==0;
}
}
复制代码
链表中每个节点都存储着下一个节点的引用,那么谁来存储链表头部的引用呢?因此,与数组不一样,链表须要额外去维护一个变量,这个变量咱们称做head,用于存储链表头的引用。git
如今向链表添加元素。
咱们须要考虑两种状况,第一种状况为:向链表头部添加元素。
github
1:newNode.next = head;// 将添加的节点的next指向head
2: head = newNode;// 将head再次指向头部
复制代码
实现代码为:数组
public void addFirst(E e){
head = new Node(e,head);
size++;
}
复制代码
还有一种状况是:在链表任意位置添加元素,这一点和在链表头部添加元素略有不一样。(广泛来说,当你选择了链表这种数据结构时,每每不会涉及向链表的中间添加元素,实现此功能是为了更加深刻地学习链表)
bash
prev=head
,每次让
prev=prev.next
,遍历index-1次,就能够得到index-1处的,也就是待插入位置的前一个位置的索引处的节点。插入的过程为:
1: newNode.next = prev.next
2: prev.next = newNode
复制代码
代码为:数据结构
public void add(int index,E e){
if(index<0 || index>size)
throw new IllegalArgumentException("Index is Illegal");
if(index==0){
// 若是在链表头部添加元素
addFirst(e);
}else{
Node prev = head;
for(int i=0;i<index-1;i++){
prev = prev.next;
}
prev.next = new Node(e,prev.next);
size++;
}
}
复制代码
若是使用head这个变量去维护链表头天然是能够的,可是咱们看到了,咱们的链表在头部添加元素时,和在其余位置添加元素的思路是不同的。有没有办法可以将链表进行优化,使得链表的头部同链表的其余位置在增删改查的操做一致呢?使用虚拟头节点就能够优化链表,解决这样的一个问题。
ide
有了dummyHead虚拟头节点后,链表的增删改查都会变的很是容易。post
public void add(int index,E e){
if(index<0 || index>e)
throw new IllegalArgumentException("Index is Illegal");
Node prev = dummyHead;
for(int i=0;i<index;i++){
prev = prev.next;
}
prev.next = new Node(e,prev.next);
size++;
}
// 在链表头添加新的元素e
public void addFirst(E e){
add(0,e);
}
// 在链表尾添加新的元素e
public void addLast(E e){
add(size,e);
}
复制代码
public E remove(int index){
if(index<0 || index>=size)
throw new IllegalArgumentException("index is Illegal");
Node prev = dummyHead;
for(int i=0;i<index;i++){
prev = prev.next;
}
E delNode = prev.next;
prev.next = prev.next.next; // prev.next = delNode.next;
delNode.next = null;
return delNode.e;
}
// 从链表中删除第一个元素,并返回
public E removeFirst(){
return remove(0);
}
// 从链表中删除最后一个元素,并返回
public E removeLast(){
return remove(size-1);
}
复制代码
// 改
public void set(int index,E e){
if(index<0 || index>=size)
throw new IllegalArgumentException("Index is Illegal");
Node prev = dummyHead;
for(int i=0;i<index;i++){
prev = prev.next;
}
prev.next.e = e;
}
// 查
public E get(int index){
if(index<0 || index>=size)
throw new IllegalArgumentException("Index is Illegal");
Node prev = dummyHead;
for(int i=0;i<index;i++){
prev = prev.next;
}
return prev.next.e;
}
// 得到链表的第一个元素
public E getFirst(){
return get(0);
}
// 得到链表的最后一个元素
public E getLast(){
return get(size-1);
}
复制代码
代码连接学习
咱们如今来看一下链表的增删改查各个操做的时间复杂度:
栈与队列是两种特殊的线性数据结构,它们都是基于某种线性数据结构做为底层进行实现的。动态数组做为底层能够实现栈与队列,而且咱们使得栈这种数据结构的各个操做均为O(1)的时间复杂度,而队列在使用数组做为底层实现时,出队操做的时间复杂度为O(n),可是循环队列则作出了改进,将队列的各个操做优化至O(1)。咱们再回顾一下栈与队列的接口方法:
Stack
public interface Stack<E> {
// 入栈
void push(E e);
// 出栈
E pop();
// 查看栈顶元素
E peek();
int getSize();
boolean isEmpty();
}
复制代码
Queue
public interface Queue<E> {
// 入队
void enqueue(E e);
// 出队
E dequeue();
// 查看队首的元素
E getFront();
int getSize();
boolean isEmpty();
}
复制代码
若是将栈与队列的底层变为链表,那么如何进行实现呢?
对于链表来讲,在链表头操做元素均为O(1)的时间复杂度,而栈是一种仅在栈顶进行push与pop的特殊的数据结构。因此咱们的思路很是简单,将链表头做为栈顶就可使得栈的相关操做为O(1)的时间复杂度了,由于代码比较简单,因此直接给出连接,再也不叙述:连接。
队列和栈不一样,由于FIFO的这种特性,就须要在队列的两头进行操做(从一端添加元素,从另外一端删除元素)。对于数组和链表两种数据结构来讲,不管是哪种,在两端进行操做的时间复杂度必是O(1)和O(n)。对于数组来讲,咱们使用了循环队列这种思想对出队操做进行优化,对于链表也必然有优化的方法,试想一下,在链表头部进行操做的时间复杂度为O(1),若是在链表的尾部也添加一个变量进行维护,那么每次在添加元素时,只须要让尾部指向新添加的元素,而且再次让维护链表尾部的这个变量指向最后一个元素不就能够了吗?假设维护链表尾部的这个变量叫作"tail",在每次向链表中添加元素时,咱们只须要tail.next = newNode;tail = newNode
就能够了,这样在链表尾部添加元素就会变为一个时间复杂度为O(1)的操做。
// 链表为底层的队列:入队
@Override
public void enqueue(E e){
Node node = new Node(e);
if(isEmpty()){
head = node;
tail = node;
}else{
tail.next = node;
tail = tail.next;
}
size++;
}
复制代码
// 链表为底层的队列:出队
@Override
public E dequeue(){
if(isEmpty())
throw new IllegalArgumentException("Queue is Empty");
Node retNode = head;
if(head==tail){
head = null;
tail = null;
}else{
head = head.next;
}
size--;
retNode.next = null;
return retNode.e;
}
复制代码