1. 程式人生 > 其它 >資料結構及演算法(資料結構篇)

資料結構及演算法(資料結構篇)

一、線性表 線性表是最基本、最簡單、也是最常用的一種資料結構。一個線性表是n個具有相同特性的資料元素的有限序列。 前驅元素: 若A元素在B元素的前面,則稱A為B的前驅元素 後繼元素: 若B元素在A元素的後面,則稱B為A的後繼元素 線性表的特徵:資料元素之間具有一種“一對一”的邏輯關係。 1. 第一個資料元素沒有前驅,這個資料元素被稱為頭結點; 2. 最後一個數據元素沒有後繼,這個資料元素被稱為尾結點; 3. 除了第一個和最後一個數據元素外,其他資料元素有且僅有一個前驅和一個後繼。 如果把線性表用數學語言來定義,則可以表示為(a1,...ai-1,ai,ai+1,...an),ai-1領先於ai,ai領先於ai+1,稱ai-1是ai的 前驅元素,ai+1是ai的後繼元素 線性表的分類: 線性表中資料儲存的方式可以是順序儲存,也可以是鏈式儲存,按照資料的儲存方式不同,可以把線性表分為順序 表和連結串列。 1.1 順序表 順序表是在計算機記憶體中以陣列的形式儲存的線性表,線性表的順序儲存是指用一組地址連續的儲存單元,依次存 儲線性表中的各個元素、使得線性表中再邏輯結構上響鈴的資料元素儲存在相鄰的物理儲存單元中,即通過資料元 素物理儲存的相鄰關係來反映資料元素之間邏輯上的相鄰關係。

1.1.1 順序表的實現

順序表API設計: 順序表的程式碼實現:
順序表的程式碼實現:
//順序表程式碼
public class SequenceList<T> {
    
//儲存元素的陣列 private T[] eles; //記錄當前順序表中的元素個數 private int N; //構造方法 public SequenceList(int capacity){ eles = (T[])new Object[capacity]; N=0; } //將一個線性表置為空表 public void clear(){ N=0; } //判斷當前線性表是否為空表 public boolean isEmpty(){ return N==0; }
//獲取線性表的長度 public int length(){ return N; } //獲取指定位置的元素 public T get(int i){ if (i<0 || i>=N){ throw new RuntimeException("當前元素不存在!"); } return eles[i]; } //向線型表中新增元素t public void insert(T t){ if (N==eles.length){
throw new RuntimeException("當前表已滿"); } eles[N++] = t; } //在i元素處插入元素t public void insert(int i,T t){ if (i==eles.length){ throw new RuntimeException("當前表已滿"); } if (i<0 || i>N){ throw new RuntimeException("插入的位置不合法"); } //把i位置空出來,i位置及其後面的元素依次向後移動一位 for (int index=N;index>i;index--){ eles[index]=eles[index-1]; } //把t放到i位置處 eles[i]=t; //元素數量+1 N++; } //刪除指定位置i處的元素,並返回該元素 public T remove(int i){ if (i<0 || i>N-1){ throw new RuntimeException("當前要刪除的元素不存在"); } //記錄i位置處的元素 T result = eles[i]; //把i位置後面的元素都向前移動一位 1.1.2 順序表的遍歷 一般作為容器儲存資料,都需要向外部提供遍歷的方式,因此我們需要給順序表提供遍歷方式。 for (int index=i;index<N-1;index++){ eles[index]=eles[index+1]; } //當前元素數量-1 N--; return result; } //查詢t元素第一次出現的位置 public int indexOf(T t){ if(t==null){ throw new RuntimeException("查詢的元素不合法"); } for (int i = 0; i < N; i++) { if (eles[i].equals(t)){ return i; } } return -1; } } //測試程式碼 public class SequenceListTest { public static void main(String[] args) { //建立順序表物件 SequenceList<String> sl = new SequenceList<>(10); //測試插入 sl.insert("姚明"); sl.insert("科比"); sl.insert("麥迪"); sl.insert(1,"詹姆斯"); //測試獲取 String getResult = sl.get(1); System.out.println("獲取索引1處的結果為:"+getResult); //測試刪除 String removeResult = sl.remove(0); System.out.println("刪除的元素是:"+removeResult); //測試清空 sl.clear(); System.out.println("清空後的線性表中的元素個數為:"+sl.length()); } }

1.1.2 順序表的遍歷

一般作為容器儲存資料,都需要向外部提供遍歷的方式,因此我們需要給順序表提供遍歷方式。 在java中,遍歷集合的方式一般都是用的是foreach迴圈,如果想讓我們的SequenceList也能支援foreach迴圈,則 需要做如下操作: 1.讓SequenceList實現Iterable介面,重寫iterator方法; 2.在SequenceList內部提供一個內部類SIterator,實現Iterator介面,重寫hasNext方法和next方法; 程式碼:
package cn.itcast.algorithm.linear;

import java.util.Iterator;

public class SequenceList<T> implements Iterable<T>{
    //儲存元素的陣列
    private T[] eles;
    //記錄當前順序表中的元素個數
    private int N;

    //構造方法
    public SequenceList(int capacity){
        //初始化陣列
        this.eles=(T[])new Object[capacity];
        //初始化長度
        this.N=0;
    }

    //將一個線性表置為空表
    public void clear(){
        this.N=0;
    }

    //判斷當前線性表是否為空表
    public boolean isEmpty(){
       return N==0;
    }

    //獲取線性表的長度
    public int length(){
        return N;
    }

    //獲取指定位置的元素
    public T get(int i){
        return eles[i];
    }

    //向線型表中新增元素t
    public void insert(T t){
        if (N==eles.length){
            resize(2*eles.length);
        }

        eles[N++]=t;
    }

    //在i元素處插入元素t
    public void insert(int i,T t){
        if (N==eles.length){
            resize(2*eles.length);
        }

        //先把i索引處的元素及其後面的元素依次向後移動一位
        for(int index=N;index>i;index--){
            eles[index]=eles[index-1];
        }
        //再把t元素放到i索引處即可
        eles[i]=t;

        //元素個數+1
        N++;
    }

    //刪除指定位置i處的元素,並返回該元素
    public T remove(int i){
        //記錄索引i處的值
        T current = eles[i];
        //索引i後面元素依次向前移動一位即可
        for(int index=i;index<N-1;index++){
            eles[index]=eles[index+1];
        }
        //元素個數-1
        N--;

        if (N<eles.length/4){
            resize(eles.length/2);
        }

        return current;
    }


    //查詢t元素第一次出現的位置
    public int indexOf(T t){
        for(int i=0;i<N;i++){
            if (eles[i].equals(t)){
                return i;
            }
        }
        return -1;
    }

    //根據引數newSize,重置eles的大小
    public void resize(int newSize){
        //定義一個臨時陣列,指向原陣列
        T[] temp=eles;
        //建立新陣列
        eles=(T[])new Object[newSize];
        //把原陣列的資料拷貝到新陣列即可
        for(int i=0;i<N;i++){
            eles[i]=temp[i];
        }
    }


    @Override
    public Iterator<T> iterator() {
        return new SIterator();
    }

    private class SIterator implements Iterator{
        private int cusor;
        public SIterator(){
            this.cusor=0;
        }
        @Override
        public boolean hasNext() {
            return cusor<N;
        }

        @Override
        public Object next() {
            return eles[cusor++];
        }
    }
}
package cn.itcast.algorithm.test;

import cn.itcast.algorithm.linear.SequenceList;


public class SequenceListTest {

    public static void main(String[] args) {
        //建立順序表物件
        SequenceList<String> sl = new SequenceList<>(10);
        //測試插入
        sl.insert("姚明");
        sl.insert("科比");
        sl.insert("麥迪");
        sl.insert(1,"詹姆斯");

        for (String s : sl) {
            System.out.println(s);
        }

        System.out.println("------------------------------------------");

        //測試獲取
        String getResult = sl.get(1);
        System.out.println("獲取索引1處的結果為:"+getResult);
        //測試刪除
        String removeResult = sl.remove(0);
        System.out.println("刪除的元素是:"+removeResult);
        //測試清空
        sl.clear();
        System.out.println("清空後的線性表中的元素個數為:"+sl.length());
    }
}

1.1.3 順序表的容量可變

在之前的實現中,當我們使用SequenceList時,先new SequenceList(5)建立一個物件,建立物件時就需要指定容 器的大小,初始化指定大小的陣列來儲存元素,當我們插入元素時,如果已經插入了5個元素,還要繼續插入數 據,則會報錯,就不能插入了。這種設計不符合容器的設計理念,因此我們在設計順序表時,應該考慮它的容量的 伸縮性。 考慮容器的容量伸縮性,其實就是改變儲存資料元素的陣列的大小,那我們需要考慮什麼時候需要改變陣列的大 小? 1.新增元素時: 新增元素時,應該檢查當前陣列的大小是否能容納新的元素,如果不能容納,則需要建立新的容量更大的陣列,我 們這裡建立一個是原陣列兩倍容量的新陣列儲存元素。 2.移除元素時: 移除元素時,應該檢查當前陣列的大小是否太大,比如正在用100個容量的陣列儲存10個元素,這樣就會造成記憶體 空間的浪費,應該建立一個容量更小的陣列儲存元素。如果我們發現數據元素的數量不足陣列容量的1/4,則建立 一個是原陣列容量的1/2的新陣列儲存元素。 順序表的容量可變程式碼:
package cn.itcast.algorithm.linear;

import java.util.Iterator;

public class SequenceList<T> implements Iterable<T>{
    //儲存元素的陣列
    private T[] eles;
    //記錄當前順序表中的元素個數
    private int N;

    //構造方法
    public SequenceList(int capacity){
        //初始化陣列
        this.eles=(T[])new Object[capacity];
        //初始化長度
        this.N=0;
    }

    //將一個線性表置為空表
    public void clear(){
        this.N=0;
    }

    //判斷當前線性表是否為空表
    public boolean isEmpty(){
       return N==0;
    }

    //獲取線性表的長度
    public int length(){
        return N;
    }

    //獲取指定位置的元素
    public T get(int i){
        return eles[i];
    }

    //向線型表中新增元素t
    public void insert(T t){
        if (N==eles.length){
            resize(2*eles.length);
        }

        eles[N++]=t;
    }

    //在i元素處插入元素t
    public void insert(int i,T t){
        if (N==eles.length){
            resize(2*eles.length);
        }

        //先把i索引處的元素及其後面的元素依次向後移動一位
        for(int index=N;index>i;index--){
            eles[index]=eles[index-1];
        }
        //再把t元素放到i索引處即可
        eles[i]=t;

        //元素個數+1
        N++;
    }

    //刪除指定位置i處的元素,並返回該元素
    public T remove(int i){
        //記錄索引i處的值
        T current = eles[i];
        //索引i後面元素依次向前移動一位即可
        for(int index=i;index<N-1;index++){
            eles[index]=eles[index+1];
        }
        //元素個數-1
        N--;

        if (N<eles.length/4){
            resize(eles.length/2);
        }

        return current;
    }


    //查詢t元素第一次出現的位置
    public int indexOf(T t){
        for(int i=0;i<N;i++){
            if (eles[i].equals(t)){
                return i;
            }
        }
        return -1;
    }

    //根據引數newSize,重置eles的大小
    public void resize(int newSize){
        //定義一個臨時陣列,指向原陣列
        T[] temp=eles;
        //建立新陣列
        eles=(T[])new Object[newSize];
        //把原陣列的資料拷貝到新陣列即可
        for(int i=0;i<N;i++){
            eles[i]=temp[i];
        }
    }


    @Override
    public Iterator<T> iterator() {
        return new SIterator();
    }

    private class SIterator implements Iterator{
        private int cusor;
        public SIterator(){
            this.cusor=0;
        }
        @Override
        public boolean hasNext() {
            return cusor<N;
        }

        @Override
        public Object next() {
            return eles[cusor++];
        }
    }
}
View Code
package cn.itcast.algorithm.test;

import cn.itcast.algorithm.linear.SequenceList;


public class SequenceListTest {

    public static void main(String[] args) {
        //建立順序表物件
        SequenceList<String> sl = new SequenceList<>(10);
        //測試插入
        sl.insert("姚明");
        sl.insert("科比");
        sl.insert("麥迪");
        sl.insert(1,"詹姆斯");

        for (String s : sl) {
            System.out.println(s);
        }

        System.out.println("------------------------------------------");

        //測試獲取
        String getResult = sl.get(1);
        System.out.println("獲取索引1處的結果為:"+getResult);
        //測試刪除
        String removeResult = sl.remove(0);
        System.out.println("刪除的元素是:"+removeResult);
        //測試清空
        sl.clear();
        System.out.println("清空後的線性表中的元素個數為:"+sl.length());
    }
}
View Code

1.1.4 順序表的時間複雜度

get(i):不難看出,不論資料元素量N有多大,只需要一次eles[i]就可以獲取到對應的元素,所以時間複雜度為O(1); insert(int i,T t):每一次插入,都需要把i位置後面的元素移動一次,隨著元素數量N的增大,移動的元素也越多,時 間複雜為O(n); remove(int i):每一次刪除,都需要把i位置後面的元素移動一次,隨著資料量N的增大,移動的元素也越多,時間復 雜度為O(n); 由於順序表的底層由陣列實現,陣列的長度是固定的,所以在操作的過程中涉及到了容器擴容操作。這樣會導致順 序表在使用過程中的時間複雜度不是線性的,在某些需要擴容的結點處,耗時會突增,尤其是元素越多,這個問題 越明顯

1.1.5 java中ArrayList實現

java中ArrayList集合的底層也是一種順序表,使用陣列實現,同樣提供了增刪改查以及擴容等功能。 1.是否用陣列實現; 2.有沒有擴容操作; 3.有沒有提供遍歷方式;

1.2 連結串列

  之前我們已經使用順序儲存結構實現了線性表,我們會發現雖然順序表的查詢很快,時間複雜度為O(1),但是增刪的 效率是比較低的,因為每一次增刪操作都伴隨著大量的資料元素移動。這個問題有沒有解決方案呢?有,我們可以 使用另外一種儲存結構實現線性表,鏈式儲存結構。   連結串列是一種物理儲存單元上非連續、非順序的儲存結構,其物理結構不能只管的表示資料元素的邏輯順序,資料元 素的邏輯順序是通過連結串列中的指標連結次序實現的。連結串列由一系列的結點(連結串列中的每一個元素稱為結點)組成, 結點可以在執行時動態生成。 那我們如何使用連結串列呢?按照面向物件的思想,我們可以設計一個類,來描述結點這個事物,用一個屬性描述這個 結點儲存的元素,用來另外一個屬性描述這個結點的下一個結點。 結點API設計: 結點類實現:
public class Node<T> {
//儲存元素
public T item;
//指向下一個結點
public Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
生成連結串列:
public static void main(String[] args) throws Exception {
//構建結點
Node<Integer> first = new Node<Integer>(11, null);
Node<Integer> second = new Node<Integer>(13, null);
Node<Integer> third = new Node<Integer>(12, null);
Node<Integer> fourth = new Node<Integer>(8, null);
Node<Integer> fifth = new Node<Integer>(9, null);
//生成連結串列
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
} 

1.2.1 單向連結串列

單向連結串列是連結串列的一種,它由多個結點組成,每個結點都由一個數據域和一個指標域組成,資料域用來儲存資料, 指標域用來指向其後繼結點。連結串列的頭結點的資料域不儲存資料,指標域指向第一個真正儲存資料的結點。

1.2.1.1 單向連結串列API設計

1.2.1.2 單向連結串列程式碼實現 //單向列表程式碼
import java.util.Iterator;
public class LinkList<T> implements Iterable<T> {
//記錄頭結點
private Node head;
//記錄連結串列的長度
private int N;
public LinkList(){
//初始化頭結點
head = new Node(null,null);
N=0;
}
//清空連結串列
public void clear(){
head.next=null;
head.item=null;
N=0;
}
//獲取連結串列的長度
public int length(){
return N;
}
//判斷連結串列是否為空
public boolean isEmpty(){
return N==0;
}
//獲取指定位置i出的元素
public T get(int i){
if (i<0||i>=N){
throw new RuntimeException("位置不合法!");
}
Node n = head.next;
for (int index = 0; index < i; index++) {
n = n.next;
}
return n.item;
}
//向連結串列中新增元素t
public void insert(T t){
//找到最後一個節點
Node n = head;
while(n.next!=null){
n = n.next;
}
Node newNode = new Node(t, null);
n.next = newNode;
//連結串列長度+1
N++;
}
//向指定位置i處,新增元素t
public void insert(int i,T t){
if (i<0||i>=N){
throw new RuntimeException("位置不合法!");
}
//尋找位置i之前的結點
Node pre = head;
for (int index = 0; index <=i-1; index++) {
pre = pre.next;
}
//位置i的結點
Node curr = pre.next;
//構建新的結點,讓新結點指向位置i的結點
Node newNode = new Node(t, curr);
//讓之前的結點指向新結點
pre.next = newNode;
//長度+1
N++;
}
//刪除指定位置i處的元素,並返回被刪除的元素
public T remove(int i){
if (i<0 || i>=N){
throw new RuntimeException("位置不合法");
}
//尋找i之前的元素
Node pre = head;
for (int index = 0; index <=i-1; index++) {
pre = pre.next;
}
//當前i位置的結點
Node curr = pre.next;
//前一個結點指向下一個結點,刪除當前結點
pre.next = curr.next;
//長度-1
N--;
return curr.item;
}
//查詢元素t在連結串列中第一次出現的位置
public int indexOf(T t){
Node n = head;
for (int i = 0;n.next!=null;i++){
n = n.next;
if (n.item.equals(t)){
return i;
}
}
return -1;
}
//結點類
private class Node{
//儲存資料
T item;
//下一個結點
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
} 
}
@Override
public Iterator iterator() {
return new LIterator();
}
private class LIterator implements Iterator<T>{
private Node n;
public LIterator() {
this.n = head;
}
@Override
public boolean hasNext() {
return n.next!=null;
}
@Override
public T next() {
n = n.next;
return n.item;
}
}
}
//測試程式碼
public class Test {
public static void main(String[] args) throws Exception {
LinkList<String> list = new LinkList<>();
list.insert(0,"張三");
list.insert(1,"李四");
list.insert(2,"王五");
list.insert(3,"趙六");
//測試length方法
for (String s : list) {
System.out.println(s);
}
System.out.println(list.length());
System.out.println("-------------------");
//測試get方法
System.out.println(list.get(2));
System.out.println("------------------------");
//測試remove方法
String remove = list.remove(1);
System.out.println(remove);
System.out.println(list.length());
System.out.println("----------------");;
for (String s : list) {
System.out.println(s);
} 
}
} 

1.2.2 雙向連結串列

雙向連結串列也叫雙向表,是連結串列的一種,它由多個結點組成,每個結點都由一個數據域和兩個指標域組成,資料域用 來儲存資料,其中一個指標域用來指向其後繼結點,另一個指標域用來指向前驅結點。連結串列的頭結點的資料域不存 儲資料,指向前驅結點的指標域值為null,指向後繼結點的指標域指向第一個真正儲存資料的結點。 按照面向物件的思想,我們需要設計一個類,來描述結點這個事物。由於結點是屬於連結串列的,所以我們把結點類作 為連結串列類的一個內部類來實現

1.2.2.1 結點API設計

1.2.2.2 雙向連結串列API設計

1.2.2.3 雙向連結串列程式碼實現

//雙向連結串列程式碼
import java.util.Iterator;
public class TowWayLinkList<T> implements Iterable<T>{
//首結點
private Node head;
//最後一個結點
private Node last;
//連結串列的長度
private int N;
public TowWayLinkList() {
last = null;
head = new Node(null,null,null);
N=0;
}
//清空連結串列
public void clear(){
last=null;
head.next=last;
head.pre=null;
head.item=null;
N=0;
}
//獲取連結串列長度
public int length(){
return N;
}
//判斷連結串列是否為空
public boolean isEmpty(){
return N==0;
}
//插入元素t
public void insert(T t){
if (last==null){
last = new Node(t,head,null);
head.next = last;
}else{
Node oldLast = last;
Node node = new Node(t, oldLast, null);
oldLast.next = node;
last = node;
}
//長度+1
N++;
}
//向指定位置i處插入元素t
public void insert(int i,T t){
if (i<0 || i>=N){
throw new RuntimeException("位置不合法");
}
//找到位置i的前一個結點
Node pre = head;
for (int index = 0; index < i; index++) {
pre = pre.next;
}
//當前結點
Node curr = pre.next;
//構建新結點
Node newNode = new Node(t, pre, curr);
curr.pre= newNode;
pre.next = newNode;
//長度+1
N++;
}
//獲取指定位置i處的元素
public T get(int i){
if (i<0||i>=N){
throw new RuntimeException("位置不合法");
}
//尋找當前結點
Node curr = head.next;
for (int index = 0; index <i; index++) {
curr = curr.next;
}
return curr.item;
}
//找到元素t在連結串列中第一次出現的位置
public int indexOf(T t){
Node n= head;
for (int i=0;n.next!=null;i++){
n = n.next;
if (n.next.equals(t)){
return i;
}
}
return -1;
}
//刪除位置i處的元素,並返回該元素
public T remove(int i){
if (i<0 || i>=N){
throw new RuntimeException("位置不合法");
}
//尋找i位置的前一個元素
Node pre = head;
for (int index = 0; index <i ; index++) {
pre = pre.next;
}
//i位置的元素
Node curr = pre.next;
//i位置的下一個元素
Node curr_next = curr.next;
pre.next = curr_next;
curr_next.pre = pre;
//長度-1;
N--;
return curr.item;
}
//獲取第一個元素
public T getFirst(){
if (isEmpty()){
return null;
}
return head.next.item;
}

//獲取最後一個元素
public T getLast(){
if (isEmpty()){
return null;
}
return last.item;
}
@Override
public Iterator<T> iterator() {
return new TIterator();
}
private class TIterator implements Iterator{
private Node n = head;
@Override
public boolean hasNext() {
return n.next!=null;
}
@Override
public Object next() {
n = n.next;
return n.item;
}
}
//結點類
private class Node{
public Node(T item, Node pre, Node next) {
this.item = item;
this.pre = pre;
this.next = next;
}
//儲存資料
public T item;
//指向上一個結點
public Node pre;
//指向下一個結點
public Node next;
}
}
//測試程式碼
public class Test {
public static void main(String[] args) throws Exception {
TowWayLinkList<String> list = new TowWayLinkList<>();
list.insert("喬峰");
list.insert("虛竹");
list.insert("段譽");
list.insert(1,"鳩摩智");
list.insert(3,"葉二孃");
for (String str : list) {
System.out.println(str);
}
System.out.println("----------------------");
String tow = list.get(2);
System.out.println(tow);
System.out.println("-------------------------");
String remove = list.remove(3);
System.out.println(remove);
System.out.println(list.length());
System.out.println("--------------------");
System.out.println(list.getFirst());
System.out.println(list.getLast());
}
}

1.2.2.4 java中LinkedList實現

java中LinkedList集合也是使用雙向連結串列實現,並提供了增刪改查等相關方法 1.底層是否用雙向連結串列實現; 2.結點類是否有三個域

1.2.3 連結串列的複雜度分析

get(int i):每一次查詢,都需要從連結串列的頭部開始,依次向後查詢,隨著資料元素N的增多,比較的元素越多,時間 複雜度為O(n) insert(int i,T t):每一次插入,需要先找到i位置的前一個元素,然後完成插入操作,隨著資料元素N的增多,查詢的 元素越多,時間複雜度為O(n); remove(int i):每一次移除,需要先找到i位置的前一個元素,然後完成插入操作,隨著資料元素N的增多,查詢的元 素越多,時間複雜度為O(n) 相比較順序表,連結串列插入和刪除的時間複雜度雖然一樣,但仍然有很大的優勢,因為連結串列的實體地址是不連續的, 它不需要預先指定儲存空間大小,或者在儲存過程中涉及到擴容等操作,,同時它並沒有涉及的元素的交換。 相比較順序表,連結串列的查詢操作效能會比較低。因此,如果我們的程式中查詢操作比較多,建議使用順序表,增刪 操作比較多,建議使用連結串列 個人學習筆記,記錄日常學習,便於查閱及加深,僅為方便個人使用。