課程《玩轉數據結構》學習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;
}
複製代碼