前言
最近花了接近一個月的時間,期間加上上課考試等,斷斷續續的,不過今天爲止,總算是把我以前--望--聞--生--畏--的數據結構全部梳理了一遍,同時用一個工程將他們全部記錄了下來,下面是這個工程的目錄截圖,可能會在後續陸續加上一些常用的操作,比如兩個棧實現一個隊列,兩個隊列實現一個棧等等,但是還是步步爲營,慢慢來,先從基礎開始。
由於我目前數據結構水平不高,主要是將基礎打牢,所以我並沒有去做一些高級用法,主要工作是弄懂常用數據結構的實現原理,我採用的辦法就是自己純手動去一個一個實現,如果遇到實再不會的就藉助網上資料,看別人的思路,然後自己再實現一遍,雖然方法比較笨,也比較費時,並且要完美的實現一個數據結構要考慮的問題是非常多的,所以我最後實現的都是基本操作,即便如此,但是我覺得還是很值得的,至少在現在看來,我對數據結構的認識比之前提高了很多,我甚至有這樣一種感受,之前我很討厭的數據結構,現在我居然好像很喜歡,甚至有點癡迷,因爲裏面很多東西是非常巧妙的,你甚至有時候會發出這樣的感嘆-----哇,真的神奇!!!
同時,爲了防止遺忘,因爲這個過程也是比較長的,所以對自己來說也是個複習吧,再次將我梳理的這個工程,從線性表開始一個一個整理出來,加深映像,好了,廢話不多說,先從線性表開始!!
目錄
1.線性表相關概念
2.線性表的基本操作
3.線性表的優缺點
4.線性表的使用場景
5.線性表的完整代碼
正文
1.線性表相關概念
爲了查漏補缺,在說明數組,先來了解一下 線性表 這個東西,線性表是最常用且最簡單的一種數據結構,簡言之,一個線性表是n個數據元素的有限序列。線性表根據表示方式不同,分爲順序表示和鏈式表示兩種,其中順序表示方式(數組)就是今天的主角啦,另外一種鏈式表示方式的線性表就是下一篇要梳理的--鏈表。
下面是順序表示的線性表的概念:
用一組地址連續的存儲單元依次存儲線性表的數據元素的數據結構。
好了,雖然說概念在實際中很少涉及到,但是有時候掌握概念讓你在實際使用的時候會知道如何取捨,以及,如果你找工作的話,萬一人家面試官問到了呢,對不,嘿嘿!
爲了方便表述,下文中的線性表都是指的順序表示的線性表。
2.線性表的基本操作
a.線性表的初始化
首先搞清楚,初始化線性表需要用什麼來初始化,那麼概念的作用來了,地址連續,自然而然的我們想到了->數組!
線性表初始化的工作可以分爲三種
1.默認情況初始化
使用默認情況時,將使用默認設定的容量來初始化數組的大小,同時默認的數據元素爲默認的0(如果申明爲int類型數組的話)
2.初始化含有一個數據元素的線性表
使用一個數據元素來初始化時,數組的容量使用默認容量,與第一種情況不同的是,將數組的第一個元素,也就是0號元素,設置爲該數據元素。
3.初始化一個數據元素,並且容量爲給定值的線性表
與第二種情況的區別是,在聲明數組並指定容量的時候,使用給定的容量值來初始化。
初始化代碼如下:
// 無參構造線性表
public SquenceList() {
this.element = new Object[DEFAULT_SIZE];// 初始化列表的空間
this.capacity = DEFAULT_SIZE;// 實際分配數組長度
}
// 初始化含有一個元素的線性表
public SquenceList(int elem) {
this();// 調用空參數構造函數
this.element[size++] = elem;
}
// 指定長度並初始化一個元素創建線性表
public SquenceList(int elem, int size) {
this.capacity = 1;// 初始化
// 擴充數組空間使得capicity的size且是2的n次方
while (this.capacity < size) {
this.capacity <<= 1;//相當於*2
}
this.element = new Object[this.capacity];
this.element[size++] = elem;
}
b.獲取線性表的長度
由於我們用數組實現的,一種可行的辦法是按順序從0號索引處開始逐個比較,並記錄長度值,直到碰到索引值爲0時爲止,這樣我們就獲取到了長度值,但是這樣有個弊端就是無法添加值爲0的元素,而且效率比較低。
另外一種可行的辦法是,我們可以爲線性表對象聲明一個成員變量 size,然後在每次添加元素或者刪除元素的時候,更新這個size值就可以了,然後要獲取長度的時候,直接返回這個size即可,這樣就省去了每次獲取長度的時候都要遍歷一遍數組。
// 獲取線性表的索引爲i處的元素(i介於0~size-1)
public int getelem(int i) {
if (i < 0 || i > size - 1) {// 檢測是否越界
throw new IndexOutOfBoundsException("線性表的索引越界:" + i);
}
return (int) this.element[i];
}
c.獲取指定值的索引
這個從索引0處開始,循環遍歷一遍數組,遇到索引對應的值和給定值相等的時候,就返回對應的索引即可,如果找完了仍然沒有找到,那麼就返回-1,用來代表未找到。
// 查找元素在線性表中的索引
public int findindex(int elem) {
for (int i = 0; i < size; i++) {
if (this.element[i].equals(elem))
return i;// 找到返回對應的索引
}
return -1;// 若沒有找到返回-1
}
d.獲取指定索引處的值
這個就很簡單了,拿到了索引,直接返回索引對應的值即可。
但是僅僅做這一步工作的話,就又有問題了,作爲一個標準的程序員,寫代碼最基本的就是--健壯性,所以還需要判斷一下索引是否越界,也就是索引值是否大於線性表的容量,以及索引小於0的情況。
// 獲取線性表的索引爲i處的元素(i介於0~size-1)
public int getelem(int i) {
if (i < 0 || i > size - 1) {// 檢測是否越界
throw new IndexOutOfBoundsException("線性表的索引越界:" + i);
}
return (int) this.element[i];
}
e.插入元素
插入相對複雜點,因爲對於數組來說,涉及到元素的移動。
考慮到程序的健壯性,在插入之前還要做的一件事就是判斷是否需要擴容,因爲有可能當前數組已經滿了,那麼就需要擴大數組的容量。如果需要擴容的話,建議是每次擴大爲之前容量的兩倍。具體的擴大容量的方法在下面會講到。
然後在指定索引處插入對應的值,分兩步,一、插入元素第i個位置空出來,從i位置開始所有元素後移一個位置,這裏可以藉助Java提供的原生方法System.arraycopy來快速實現;二、指定i索引處的值爲給定值。
當然最後不要忘了將記錄數組長度的變量size加一。
也可以加一些額外的特殊需求的方法,比如在線性表末尾插入元素,這個就是直接調用一下上面寫好的方法即可,只不過默認的插入索引爲數組長度代表的索引。
// 插入一個元素到線性表的第i個索引處
public void insert(int elem, int i) {
// 是否需要擴充容量
this.ensureCapicty(size + 1);
// 插入元素第i個位置空出來從i位置開始所有元素後移一個位置
System.arraycopy(this.element, i, this.element, i + 1, size - i);
// 將元素插入到指定位置
this.element[i] = elem;
// 當前容量增加1
this.size++;
}
// 在線性表末尾插入元素
public void add(int elem) {
this.insert(elem, this.size);
}
f.刪除元素
仍然考慮到程序的健壯性,首先判斷需要判斷的是刪除元素的索引是否是合法的,然後獲取刪除索引處的值,保存用於最後返回,看到這你可能會很奇怪,我刪除元素就直接做刪除元素的操作就可以了嘛,爲啥還要給這個刪除元素的方法弄個返回值呢,其實我剛開始也有這樣的困惑,但是你仔細想想,在開發調試的時候,刪除了一個元素,如何知道你刪除的元素是不是你想要刪除的那個元素呢,如果我不給這個方法添加返回值的話,那麼是不是還要額外重新寫代碼,或者通過其它方式來判斷呢?所以多思考,參考優秀的人的優秀思想是很有好處的,嘿嘿,好了,不扯遠了。
刪除一個元素仍然分爲兩步,一、獲取需要前移的元素個數,如果大於0的話,也就是有元素需要前移,那麼使用System.arraycopy方法進行相關元素的前移即可;二、清空最後一個元素,也就是將size-1處的值置空。
最後,別忘了返回一下開頭獲取到的值哦。
可添加的相關額外方法有:刪除數組最後一個元素,removeLast,只需要調用一下上面封裝好的方法即可。
// 刪除線性表中第i個元素並返回該處的值
public int delete(int i) {
if (i < 0 || i > size - 1) {// 檢測刪除位置對不對
throw new IndexOutOfBoundsException("刪除位置索引越界:" + i);
}
// 獲得i處的元素值
int del = (int) this.element[i];
// 刪除元素後從i+1位置開始元素要前移
int moved = this.size - i - 1;// 需要移動元素的個數
if (moved > 0) {
System.arraycopy(this.element, i + 1, this.element, i, moved);
}
// 清空最後一個元素
this.element[--size] = null;
return del;
}
// 移除線性表中最後一個元素
public int remove() {
return this.delete(size - 1);
}
g.容量擴充
容量擴充,較好的擴充是每次擴充爲之前兩倍,同時也要考慮到擴大了兩倍仍然不夠的情況,所以最好使用while循環來控制,直到容量足夠爲止,然後將原來的元素拷貝到新的位置上。
// 擴充線性表的容量
private void ensureCapicty(int currentCapicty) {
if (currentCapicty > this.capacity) {// 若實際的所需容量大於實際的容量則擴充使得實際容量大於所需容量且是2的次方
while (this.capacity < currentCapicty) {
this.capacity <<= 1;
}
// 將原來的元素拷貝到新的位置上
this.element = Arrays.copyOf(this.element, this.capacity);
}
}
h.判斷線性表是否爲空
這個也很簡單啦,一句代碼size==0即可。
i.清空線性表
清空線性表分兩步:一、置size爲0;二、置線性表所有的元素爲空。
// 清空線性表
public void clear() {
// 將所有元素賦值爲null
Arrays.fill(this.element, null);
this.size = 0;
}
3.線性表的優缺點
優點:查找快(知道索引的情況下,只需O(1)的時間即可定位並獲取到值)
缺點:插入刪除效率低下,因爲順序存儲的原因,插入刪除就會導致元素的不斷移動。
4.線性表的使用場景
當需要的操作大多數情況下爲查找操作時,那麼就可以採用線性表。
5.線性表的完整代碼
package SquenceList;
import java.util.Arrays;
public class SquenceList {
private int DEFAULT_SIZE = 4;// 線性表的默認長度空間
private int capacity;// 線性表的實際分配數組長度
private int size = 0;// 線性表的當前元素個數及線性表的長度
private Object[] element;// 數據元素封裝一個數組
// 無參構造線性表
public SquenceList() {
this.element = new Object[DEFAULT_SIZE];// 初始化列表的空間
this.capacity = DEFAULT_SIZE;// 實際分配數組長度
}
// 初始化含有一個元素的線性表
public SquenceList(int elem) {
this();// 調用空參數構造函數
this.element[size++] = elem;
}
// 指定長度並初始化一個元素創建線性表
public SquenceList(int elem, int size) {
this.capacity = 1;// 初始化
// 擴充數組空間使得capicity的size且是2的n次方
while (this.capacity < size) {
this.capacity <<= 1;//相當於*2
}
this.element = new Object[this.capacity];
this.element[size++] = elem;
}
// 獲得線性表的長度
public int length() {
return this.size;
}
// 獲取線性表的索引爲i處的元素(i介於0~size-1)
public int getelem(int i) {
if (i < 0 || i > size - 1) {// 檢測是否越界
throw new IndexOutOfBoundsException("線性表的索引越界:" + i);
}
return (int) this.element[i];
}
// 查找元素在線性表中的索引
public int findindex(int elem) {
for (int i = 0; i < size; i++) {
if (this.element[i].equals(elem))
return i;// 找到返回對應的索引
}
return -1;// 若沒有找到返回-1
}
// 擴充線性表的容量
private void ensureCapicty(int currentCapicty) {
if (currentCapicty > this.capacity) {// 若實際的所需容量大於實際的容量則擴充使得實際容量大於所需容量且是2的次方
while (this.capacity < currentCapicty) {
this.capacity <<= 1;
}
// 將原來的元素拷貝到新的位置上
this.element = Arrays.copyOf(this.element, this.capacity);
}
}
// 插入一個元素到線性表的第i個索引處
public void insert(int elem, int i) {
// 是否需要擴充容量
this.ensureCapicty(size + 1);
// 插入元素第i個位置空出來從i位置開始所有元素後移一個位置
System.arraycopy(this.element, i, this.element, i + 1, size - i);
// 將元素插入到指定位置
this.element[i] = elem;
// 當前容量增加1
this.size++;
}
// 在線性表末尾插入元素
public void add(int elem) {
this.insert(elem, this.size);
}
// 刪除線性表中第i個元素並返回該處的值
public int delete(int i) {
if (i < 0 || i > size - 1) {// 檢測刪除位置對不對
throw new IndexOutOfBoundsException("刪除位置索引越界:" + i);
}
// 獲得i處的元素值
int del = (int) this.element[i];
// 刪除元素後從i+1位置開始元素要前移
int moved = this.size - i - 1;// 需要移動元素的個數
if (moved > 0) {
System.arraycopy(this.element, i + 1, this.element, i, moved);
}
// 清空最後一個元素
this.element[--size] = null;
return del;
}
// 移除線性表中最後一個元素
public int remove() {
return this.delete(size - 1);
}
// 判斷線性表是否爲空
public boolean empty() {
return this.size == 0;
}
// 清空線性表
public void clear() {
// 將所有元素賦值爲null
Arrays.fill(this.element, null);
this.size = 0;
}
// 覆寫toString
public String toString() {
if (this.size == 0) {
return "[]";
} else {
// 返回字符串的字符串表示
StringBuffer sb = new StringBuffer("[");
for (int i = 0; i < this.size - 1; i++) {
sb.append(this.element[i].toString() + ",");
}
sb.append(this.element[this.size - 1].toString() + "]");
return sb.toString();
}
}
}
最後再附上一個測試線性表的例子
public static void main(String[] args) {
// TODO Auto-generated method stub
SquenceList list=new SquenceList();
list.add(0);
list.add(1);
list.add(2);
list.add(3);
list.insert(4, 0);
System.out.println(list);
System.out.println("index:3 ="+list.getelem(3));
System.out.println("data:3 ="+list.findindex(3));
list.delete(2);
System.out.println(list);
list.delete(0);
System.out.println(list);
list.delete(2);
System.out.println(list);
}
結語
好啦,線性表是數據結構裏最簡單的,所以比較輕鬆,但是也暗藏了一些需要注意的問題,下一篇:鏈表。