玩轉資料結構——第三章:最基礎的動態資料結構:連結串列
阿新 • • 發佈:2018-11-03
內容概括:
- 3-1.什麼是連結串列
- 3-2.在連結串列中新增元素
- 3-3.使用連結串列的虛擬頭結點
- 3-4.連結串列的遍歷,查詢和修改
- 3-5.從連結串列中刪除元素
- 3-6.使用連結串列實現棧
- 3-7.帶有尾指標的連結串列:使用連結串列實現佇列
3-1.什麼是連結串列
- 連結串列:真正的動態資料結構
- 最簡單的動態資料結構
- 更深入的理解引用(或者指標)
- 更深入的理解遞迴
- 輔助組成其他資料結構
連結串列Linked List
連結串列像火車一樣,每個節點相當於一節車廂,每一節車廂存放真正的資料,每一節車廂之間要有next連結
- 資料儲存在“節點”(Node)中
- 如果一個節點的next為null,則已經到了連結串列末尾了
- 優點:真正的動態,不需要處理固定容量的問題
- 缺點:與陣列比較喪失了隨機訪問的能力
陣列和連結串列相比
組成連結串列的內部節點和next類:
- 陣列最好用於索引有語意的情況。scores[2]
- 最大的優點:支援快速查詢
- 連結串列不適合用於索引有語意的情況
- 最大優點:動態
public class LinkedList<E> { //私有內部類 private class Node {//只有在連結串列內才能訪問 public E e;//節點資料 public Node next;//節點next public Node(E e, Node next) { this.e = e;//使用者傳進來的e賦值給當前節點的e this.next = next;//使用者傳來的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; } }
3-2.在連結串列中新增元素
1.在連結串列頭新增元素
//在連結串列頭新增新的元素
public void addFirst(E e){
// Node node=new Node();
//// node.next=head;
//// head=node;
head=new Node(e,head);//當前節點為使用者傳進來的節點,當前節點的next為使用者上一個頭結點
size++;
}
2.在連結串列中間新增元素
如果順序調換。
- 關鍵:找到要新增的節點的前一個節點
//在連結串列的index(0-based)位置新增新的元素e
//在連結串列中不是一個常用操作,練習用:
public void add(int index,E e){
if(index<0||index>size){//判斷index的合法性
throw new IllegalArgumentException("Add is Fail,Illega Index"); }
if(index==0)
addFirst(e);
else{
Node prev=head;
for (int i=0;i<index-1;i++){//讓prev找到index-1位置
prev=prev.next;//讓prev挪向待插入位置的前一個節點
// Node node =new Node(e);//1
// node.next=prev.next;//2
// prev.next=node;//3
//new Node(e,prev.next);完成前兩句話的任務
//prev.next=..完成第三句話的任務
prev.next=new Node(e,prev.next);
size++;
}}}
//在連結串列的末尾新增新的元素e
public void addLast(E e){
add(size,e);
}
3-3.連結串列設定虛擬頭結點
元素從0開始,dummyHead是0位置的前一個節點
- 在沒有虛擬頭結點的連結串列中連結串列頭新增元素和連結串列任意位置上新增元素的操作不同
- 因為連結串列頭沒有前一個節點,為了實現兩個操作統一,設定一個虛擬頭結點。
public LinkedList(){
dummyHead=new Node(null,null);//建立一個虛擬頭結點
size=0; }
//獲取連結串列中元素的個數
public int getSize(){
return size; }
//返回連結串列是否為空
public boolean isEmpty(){
return size==0; }
//在連結串列的index(0-based)位置新增新的元素e
//在連結串列中不是一個常用操作,練習用:
public void add(int index,E e){
if(index<0||index>size){//判斷index的合法性
throw new IllegalArgumentException("Add is Fail,Illega Index");
}
Node prev=dummyHead;//虛擬頭結點,0位置的元素的前一個節點
for (int i=0;i<index;i++)//讓prev找到index位置的前一個位置
prev=prev.next;//讓prev挪向待插入位置的前一個節點
prev.next=new Node(e,prev.next);
size++;
}
//在連結串列頭新增新的元素
public void addFirst(E e){
add(0,e);
}
//在連結串列的末尾新增新的元素e
public void addLast(E e){
add(size,e);
}
3-4.連結串列的遍歷、查詢和修改
獲得連結串列的第index個元素
//獲得連結串列的第index(0-based)個位置的元素
public E get(int index){
//判斷index的合法性
if(index<0||index>size){//判斷index的合法性
throw new IllegalArgumentException("Get is Fail,Illega Index");
}
Node cur=dummyHead.next;//從dummyHead的下一個節點開始遍歷
for(int i=0;i<index;i++)
cur=cur.next;
return cur.e;//返回的就是index位置的元素
}
//獲得連結串列的第一個元素
public E getFirst(){
return get(0);
}
//獲得連結串列的最後一個元素
public E getLast(){
return get(size-1);//從0開始算的 0個位置有一個元素 size=1
}
修改index位置的元素
//修改連結串列的第index(0-based)個位置的元素
public void set(int index,E e){
//判斷index的合法性
if(index<0||index>size){//判斷index的合法性
throw new IllegalArgumentException("Set is Fail,Illega Index");
}
Node cur=dummyHead.next;
for (int i=0;i<index;i++)
cur=cur.next;
cur.e=e;//讓第index位置的e變成新的額
}
查詢連結串列有無此元素
//查詢連結串列是否有元素e
public boolean contains(E e) {
Node cur = dummyHead.next;
while (cur != null) {//沒到末尾
if (cur.e.equals(e))//
return true;
cur = cur.next;//否則繼續往下找
}
return false;//找不到
}
@Override
public String toString(){
StringBuilder res=new StringBuilder();
Node cur=dummyHead.next;
// for(Node cur=dummyHead.next;cur!=null;cur=cur.next)等價
while(cur!=null){
res.append(cur+"->");
cur=cur.next;
}
res.append("NULL");//到達節尾
return res.toString();
}
Main_Activity.java中實現
public class Main {
public static void main(String[] args) {
LinkedList<Integer> linkedList=new LinkedList<>();
for(int i=0;i<5;i++){
linkedList.addFirst(i);
System.out.println(linkedList);
}
linkedList.add(2,666);//在索引為2的位置新增元素666
System.out.println(linkedList);
}
}
結果
0->NULL
1->0->NULL
2->1->0->NULL
3->2->1->0->NULL
4->3->2->1->0->NULL
4->3->666->2->1->0->NULL
3-5.從連結串列中刪除元素
連結串列刪除操作的實現:
//從連結串列中刪除index元素的草(0-based)
public E remove(int index){
//判斷index的合法性
if(index<0||index>size){//判斷index的合法性
throw new IllegalArgumentException("Set is Fail,Illega Index");
}
Node prev=dummyHead;
for (int i=0;i<index;i++){
prev=prev.next;//存待刪除之前的節點
}
Node retNode=prev.next;//要刪除的節點retNode
prev.next=retNode.next;//跳過retNode
retNode=null;
size--;
return retNode.e;//返回刪除的的元素
}
//刪除第一個元素
public E removeFirst(){
return remove(0);
}
//刪除最後一個元素
public E removeLast(){
return remove(size-1);
}
Main實現
public class Main {
public static void main(String[] args) {
LinkedList<Integer> linkedList=new LinkedList<>();
for(int i=0;i<5;i++){
linkedList.addFirst(i);
System.out.println(linkedList); }
linkedList.add(2,666);//在索引為2的位置新增元素666
System.out.println(linkedList);
//刪除2的元素
linkedList.remove(2);
System.out.println(linkedList);
//刪除第一個元素
linkedList.removeFirst();
System.out.println(linkedList);
//刪除最後的元素
linkedList.removeLast();
System.out.println(linkedList);
}
}
結果
0->NULL
1->0->NULL
2->1->0->NULL
3->2->1->0->NULL
4->3->2->1->0->NULL
4->3->666->2->1->0->NULL
4->3->2->1->0->NULL
3->2->1->0->NULL
3->2->1->NULL
連結串列的時間複雜度分析:
新增操作:O(n)
- addLast(e) O(n)
- addFirst(e) O(1)
- add(index,e) O(n/2)=O(n)
刪除操作:O(n)
- removeLast(e) O(n)
- removeFirst(e) O(1)
- remove(index,e) O(n/2)=O(n)
修改操作:O(n)
- set(index,e) O(n)
查詢操作: O(n)
- get(e) O(n)
- contains(e) O(n)
3-6.使用連結串列實現棧
要想實現上一節只對連結串列頭進行操作,可以將連結串列實現棧,將連結串列頭看做棧頂
public interface Stack<E> {
int getSize();//返回棧的元素個數
boolean isEmpty();//返回棧是否為空
void push(E e);//入棧
E pop();//出棧
E peek();//檢視棧末尾的元素
}
public class LinkedListStack<E> implements Stack<E>{
private LinkedList<E> list;
public LinkedListStack(){
list=new LinkedList<>();
}
@Override
public int getSize(){
return list.getSize();
}
@Override
public boolean isEmpty(){
return list.isEmpty();
}
//給棧頂新增一個元素
@Override
public void push(E e){
list.addFirst(e);//連結串列頭是棧頂
}
//從棧頂取出元素
@Override
public E pop(){
return list.removeFirst();
}
//看棧頂的元素
@Override
public E peek(){
return list.getFirst();
}
@Override
public String toString(){
StringBuilder res=new StringBuilder();
res.append("Stack:top ");
res.append(list);
return res.toString();
}
//測試用例
public static void main(String[] args) {
LinkedListStack<Integer> stack=new LinkedListStack<>();
for(int i=0;i<5;i++){
stack.push(i);//入棧
System.out.println(stack);
}
stack.pop();//出棧
System.out.println(stack);
}
}
結果:
Stack:top 0->NULL
Stack:top 1->0->NULL
Stack:top 2->1->0->NULL
Stack:top 3->2->1->0->NULL
Stack:top 4->3->2->1->0->NULL
Stack:top 3->2->1->0->NULL
陣列棧和連結串列的棧的比較
public class MainTest {//Queue的多型性
//測試使用q執行opCount個enqueue和dequeue操作兩種佇列需要花費的時間,單位秒
private static double testStack(Stack<Integer> stack,int opCount){
long startTime=System.nanoTime();//計算當時時間,單位納秒
//。。。。。你要進行的操作
//生成隨機數
Random random=new Random();
for(int i=0;i<opCount;i++){
//入隊插入隨機數
stack.push(random.nextInt(Integer.MAX_VALUE));//0-Integer最大值
}
for(int i=0;i<opCount;i++){
//出隊操作
stack.pop();
}
long endTime=System.nanoTime();//計算結束時間,單位納秒
return (endTime-startTime)/1000000000.0;//轉換單位成秒
}
public static void main(String[] args) {
int opCount=100000;
//陣列棧的操作
ArrayStack<Integer> arrayStack=new ArrayStack<>();
double time1=testStack(arrayStack,opCount);
System.out.println("ArrayStack Time="+time1+"s");
//連結串列實現的棧的操作
LinkedListStack<Integer> linkedListStack=new LinkedListStack<>();
double time2=testStack(linkedListStack,opCount);
System.out.println("LinkedListStack Time="+time2+"s");
}
}
結果:並不是所有情況LinkedListStack都比ArrayStack時間快
ArrayStack Time=0.037542493s
LinkedListStack Time=0.021257096s
3-7.帶有尾指標的連結串列:使用連結串列實現佇列
在隊尾中新增一個tail屬性,來控制元素的入隊操作,時間複雜度為O(1)
head屬性來控制出隊操作,時間複雜度也為O(1)
基本屬性
public class LinkedListQueue<E> implements Queue<E> {
//私有內部類
private class Node {//只有在連結串列內才能訪問
public E e;//節點資料
public Node next;//節點next
public Node(E e, Node next) {
this.e = e;//使用者傳進來的e賦值給當前節點的e
this.next = next;//使用者傳來的next當前節點的next }
public Node(E e) {
this(e, null); }
public Node() {
this(null, null); }
@Override
public String toString() {
return e.toString(); }}
private Node head, tail;//頭尾節點
private int size;
public LinkedListQueue() {
head = null;
tail = null;
size = 0; }
@Override
public int getSize() {
return size; }
@Override
public boolean isEmpty() {
return size == 0;
}}
進隊操作
//進隊操作、在隊尾中插入元素
@Override
public void enqueue(E e){
if (tail == null) {//連結串列尾如為空,則隊首head也為空
tail = new Node(e);
head = tail;
} else {
tail.next = new Node(e);
tail = tail.next;
}
size++;
}
出隊操作
//出隊操作,從隊首
@Override
public E dequeue(){
if(isEmpty())
throw new IllegalArgumentException("Cannot dequeue from an Empty queue");
Node reNode=head;
head=head.next;
reNode.next=null;
if(head==null)
tail=null;
size--;
return reNode.e;
}
//獲取隊首元素
@Override
public E getFront(){
if(isEmpty())
throw new IllegalArgumentException("Queue is empty");
return head.e;
}
重寫toString方法
@Override
public String toString(){
StringBuilder res=new StringBuilder();
res.append("Queue:front ");
Node cur=head;
while (cur!=null){
res.append(cur+"->");
cur=cur.next;
}
res.append("NULL tail");
return res.toString();
}
測試方法:
public static void main(String[] arg){
LinkedListQueue<Integer> queue=new LinkedListQueue<>();
for(int i=0;i<10;i++){
queue.enqueue(i);
System.out.println(queue);
//每插入佇列3個元素,取出一個元素
if(i%3==2){//0、1、2 2對3取餘為2
queue.dequeue();
System.out.println(queue);
} } }
結果:
Queue:front 0->NULL tail
Queue:front 0->1->NULL tail
Queue:front 0->1->2->NULL tail
Queue:front 1->2->NULL tail
Queue:front 1->2->3->NULL tail
Queue:front 1->2->3->4->NULL tail
Queue:front 1->2->3->4->5->NULL tail
Queue:front 2->3->4->5->NULL tail
Queue:front 2->3->4->5->6->NULL tail
Queue:front 2->3->4->5->6->7->NULL tail
Queue:front 2->3->4->5->6->7->8->NULL tail
Queue:front 3->4->5->6->7->8->NULL tail
Queue:front 3->4->5->6->7->8->9->NULL tail
(轉自發條魚)