數據結構與算法是程序設計的兩大基礎,大型的IT企業面試時也會出數據結構和算法的題目,
它可以說明你是否有良好的邏輯思維,如果你具備良好的邏輯思維,即使技術存在某些缺陷,面試公司也會認為你很有培養價值,至少在一段時間之后,技術可以很快得到提高。同時,它也是軟考的重點,我們需要對這部分的內容進行一下總結。
我們先看一下數據結構和算法的整體內容。
1、線性表
概念:
數據元素的排列方式是線性的。
分類:
分類規則是根據上圖中元素的存儲結構來划分的。
(1)順序表
基本思想:元素的存儲空間是連續的。在內存中是以順序存儲,內存划分的區域是連續的。存儲結構如下圖:
(2)鏈表
基本思想:元素的存儲空間是離散的,單獨的(物理),它們可以通過在邏輯上指針的聯系使得它成為了整體的鏈表。存儲結構如下圖:
1.單鏈表
2.循環鏈表
·
3.雙鏈表(雙向循環表)
(圖有點小問題 :最后一個節點的 指針域 也指向頭結點)
三者的區別(從上面三個圖我們可以總結出來):
1、它們都有數據域(data(p))和指針域(next(p)),但是從圖中可以看出雙鏈表有兩個指針域,一個指向它的前節點,一個指向它的后節點。
2、單鏈表最后一個節點的指針域為空,沒有后繼節點;循環鏈表和雙鏈表最后一個節點的指針域指向頭節點,下一個結點為頭節點,構成循環;
3、單鏈表和循環鏈表只可向一個方向遍歷;雙鏈表和循環鏈表,首節點和尾節點被連接在一起,可視為“無頭無尾”;雙鏈表可以向兩個方向移動,靈活度更大。
線性表操作:
理解了順序表和鏈表的基本思想之后,線性表的操作是簡單,並且網上有很多講解插入和刪除結點的博客,在這里我就不過多的介紹了。
順序表和鏈表的對比:
棧和隊列是特殊的線性表,既然特殊就有不同點。
2、棧
基本思想:后進先出(先進后出)即棧中元素被處理時,按后進先出的順序進行,棧又叫后進先出表(LIFO)。
舉例:
日常生活中有很多棧的例子。例如,放在書桌上的一摞書,只能從書頂上拿走一本書,書也只能放在頂上。如下圖所示:
3、隊列
基本思想:先進先出即先被接收的元素將先被處理,又叫先進先出表(FIFO)。如下圖所示:
舉例:
隊列的例子,生活中更多。比如:買車票排隊,排頭最先買到車票,新來的排的隊尾;進車站時,安檢行李,先進去的最先出來,后進去的后出來。
分類:
1.順序隊列
如下圖所示:
順序隊列的操作,要判斷隊滿和隊空的標志,從圖中我們可以總結得到:
1.隊空:head = tail
2.隊滿:tail = m
2.循環隊列
如下圖所示:
循環隊列的操作,要判斷隊空和隊滿的情況,從圖中我們可以總結得到:
1.隊空:head = tail
2.隊滿:tail + 1 = head(在隊列中會留一個空着的空間,所以要加1)
總結
線性表真的很簡單。
----------------------------------------------------------------------------------------------------
數據結構中的線性表,對應着Collection接口中的List接口。
在本節中,我們將做以下三件事
第一。我們先來看看線性表的特征
第二,自己用JAVA實現List
第三,對比的線性表、鏈式表性能,以及自己的List性能與JDKList性能對比
線性表特征:
第一,一個特定的線性表,應該是用來存放特定的某一個類型的元素的(元素的“同一性”)
第二, 除第一個元素外,其他每一個元素有且僅有一個直接前驅;除最后一個元素外,其他每一個元素有且僅有一個直接后繼(元素的“序偶性”)
第三, 元素在線性表中的“下標”唯一地確定該元素在表中的相對位置(元素的“索引性”)
又,一.線性表只是數據的一種邏輯結構,其具體存儲結構可以為順序存儲結構和鏈式儲存結構來完成,對應可以得到順序表和鏈表,
二.對線性表的入表和出表順序做一定的限定,可以得到特殊的線性表,棧(FILO)和隊列(FIFO)
自己實現線性表之順序表
思路:
1. 順序表因為采用順序存儲形式,所以內部使用數組來存儲數據
2.因為存儲的具體對象類型不一定,所以采用泛型操作
3.數組操作優點:1.通過指針快速定位到下表,查詢快速
缺點:1.數組聲明時即需要確定數組大小。當操作中超過容量時,則需要重新聲明數組,並且復制當前所有數據
2.當需要在中間進行插入或者刪除時,則需要移動大量元素(size-index個)
具體實現代碼如下
- /**
- * 自己用數組實現的線性表
- */
- public class ArrayList<E> {
- Object[] data = null;// 用來保存此隊列中內容的數組
- int current; // 保存當前為第幾個元素的指標
- int capacity; // 表示數組大小的指標
- /**
- * 如果初始化時,未聲明大小,則默認為10
- */
- public ArrayList() {
- this(10);
- }
- /**
- * 初始化線性表,並且聲明保存內容的數組大小
- * @param initalSize
- */
- public ArrayList(int initalSize) {
- if (initalSize < 0) {
- throw new RuntimeException("數組大小錯誤:" + initalSize);
- } else {
- this.data = new Object[initalSize];
- this.current = 0;
- capacity = initalSize;
- }
- }
- /**
- * 添加元素的方法 添加前,先確認是否已經滿了
- * @param e
- * @return
- */
- public boolean add(E e) {
- ensureCapacity(current);// 確認容量
- this.data[current] = e;
- current++;
- return true;
- }
- /**
- * 確認系統當前容量是否滿足需要,如果滿足,則不執行操作 如果不滿足,增加容量
- * @param cur 當前個數
- */
- private void ensureCapacity(int cur) {
- if (cur == capacity) {
- // 如果達到容量極限,增加10的容量,復制當前數組
- this.capacity = this.capacity + 10;
- Object[] newdata = new Object[capacity];
- for (int i = 0; i < cur; i++) {
- newdata[i] = this.data[i];
- }
- this.data = newdata;
- }
- }
- /**
- * 得到指定下標的數據
- * @param index
- * @return
- */
- public E get(int index) {
- validateIndex(index);
- return (E) this.data[index];
- }
- /**
- * 返回當前隊列大小
- * @return
- */
- public int size() {
- return this.current;
- }
- /**
- * 更改指定下標元素的數據為e
- * @param index
- * @param e
- * @return
- */
- public boolean set(int index, E e) {
- validateIndex(index);
- this.data[index] = e;
- return true;
- }
- /**
- * 驗證當前下標是否合法,如果不合法,拋出運行時異常
- * @param index 下標
- */
- private void validateIndex(int index) {
- if (index < 0 || index > current) {
- throw new RuntimeException("數組index錯誤:" + index);
- }
- }
- /**
- * 在指定下標位置處插入數據e
- * @param index 下標
- * @param e 需要插入的數據
- * @return
- */
- public boolean insert(int index, E e) {
- validateIndex(index);
- Object[] tem = new Object[capacity];// 用一個臨時數組作為備份
- //開始備份數組
- for (int i = 0; i < current; i++) {
- if (i < index) {
- tem[i] = this.data[i];
- }else if(i==index){
- tem[i]=e;
- }else if(i>index){
- tem[i]=this.data[i-1];
- }
- }
- this.data=tem;
- return true;
- }
- /** * 刪除指定下標出的數據<br>
- * @param index<br>
- * @return<br>
- */
- public boolean delete(int index){
- validateIndex(index);
- Object[] tem = new Object[capacity];// 用一個臨時數組作為備份
- //開始備份數組
- for (int i = 0; i < current; i++) {
- if (i < index) {
- tem[i] = this.data[i];
- }else if(i==index){
- tem[i]=this.data[i+1];
- }else if(i>index){
- tem[i]=this.data[i+1];
- }
- }
- this.data=tem;
- return true;
- }
- }
自己實現線性表之鏈表
思路:1.鏈表采用鏈式存儲結構,在內部只需要將一個一個結點鏈接起來。(每個結點中有關於此結點下一個結點的引用)
鏈表操作優點:1.因為每個結點記錄下個結點的引用,則在進行插入和刪除操作時,只需要改變對應下標下結點的引用即可
缺點:1.要得到某個下標的數據,不能通過下標直接得到,需要遍歷整個鏈表。
實現代碼如下
- /**
- * 自己用鏈式存儲實現的線性表
- */
- public class LinkedList<E> {
- private Node<E> header = null;// 頭結點
- int size = 0;// 表示數組大小的指標
- public LinkedList() {
- this.header = new Node<E>();
- }
- public boolean add(E e) {
- if (size == 0) {
- header.e = e;
- } else {
- // 根據需要添加的內容,封裝為結點
- Node<E> newNode = new Node<E>(e);
- // 得到當前最后一個結點
- Node<E> last = getNode(size-1);
- // 在最后一個結點后加上新結點
- last.addNext(newNode);
- }
- size++;// 當前大小自增加1
- return true;
- }
- public boolean insert(int index, E e) {
- Node<E> newNode = new Node<E>(e);
- // 得到第N個結點
- Node<E> cNode = getNode(index);
- newNode.next = cNode.next;
- cNode.next = newNode;
- size++;
- return true;
- }
- /**
- * 遍歷當前鏈表,取得當前索引對應的元素
- *
- * @return
- */
- private Node<E> getNode(int index) {
- // 先判斷索引正確性
- if (index > size || index < 0) {
- throw new RuntimeException("索引值有錯:" + index);
- }
- Node<E> tem = new Node<E>();
- tem = header;
- int count = 0;
- while (count != index) {
- tem = tem.next;
- count++;
- }
- return tem;
- }
- /**
- * 根據索引,取得該索引下的數據
- *
- * @param index
- * @return
- */
- public E get(int index) {
- // 先判斷索引正確性
- if (index >= size || index < 0) {
- throw new RuntimeException("索引值有錯:" + index);
- }
- Node<E> tem = new Node<E>();
- tem = header;
- int count = 0;
- while (count != index) {
- tem = tem.next;
- count++;
- }
- E e = tem.e;
- return e;
- }
- public int size() {
- return size;
- }
- /**
- * 設置第N個結點的值
- *
- * @param x
- * @param e
- * @return
- */
- public boolean set(int index, E e) {
- // 先判斷索引正確性
- if (index > size || index < 0) {
- throw new RuntimeException("索引值有錯:" + index);
- }
- Node<E> newNode = new Node<E>(e);
- // 得到第x個結點
- Node<E> cNode = getNode(index);
- cNode.e = e;
- return true;
- }
- /**
- * 用來存放數據的結點型內部類
- */
- class Node<e> {
- private E e;// 結點中存放的數據
- Node<E> next;// 用來指向該結點的下一個結點
- Node() { }
- Node(E e) {
- this.e = e;
- }
- // 在此結點后加一個結點
- void addNext(Node<E> node) {
- next = node;
- }
- }
- }
自己實現線性表之棧
棧是限定僅允許在表的同一端(通常為“表尾”)進行插入或刪除操作的線性表。
允許插入和刪除的一端稱為棧頂(top),另一端稱為棧底(base)
特點:后進先出 (LIFO)或,先進后出(FILO)
因為棧是限定線的線性表,所以,我們可以調用前面兩種線性表,只需要對出棧和入棧操作進行設定即可
具體實現代碼
- /**
- * 自己用數組實現的棧
- */
- public class ArrayStack<E> {
- private ArrayList<E> list=new ArrayList<E>();//用來保存數據線性表<br> private int size;//表示當前棧元素個數
- /**
- * 入棧操作
- * @param e
- */
- public void push(E e){
- list.add(e);
- size++;
- }
- /**
- * 出棧操作
- * @return
- */
- public E pop(){
- E e= list.get(size-1);
- size--;
- return e;
- }
- }
至於用鏈表實現棧,則只需要把保存數據的順序表改成鏈表即可,此處就不給出代碼了
自己實現線性表之隊列
與棧類似
隊列是只允許在表的一端進行插入,而在另一端刪除元素的線性表。
在隊列中,允許插入的一端叫隊尾(rear),允許刪除的一端稱為隊頭(front)。
特點:先進先出 (FIFO)、后進后出 (LILO)
同理,我們也可以調用前面兩種線性表,只需要對隊列的入隊和出隊方式進行處理即可
- package cn.javamzd.collection.List;
- /**
- * 用數組實現的隊列
- */
- public class ArrayQueue<E> {
- private ArrayList<E> list = new ArrayList<E>();// 用來保存數據的隊列
- private int size;// 表示當前棧元素個數
- /**
- * 入隊
- * @param e
- */
- public void EnQueue(E e) {
- list.add(e);
- size++;
- }
- /**
- * 出隊
- * @return
- */
- public E DeQueue() {
- if (size > 0) {
- E e = list.get(0);
- list.delete(0);
- return e;
- }else{
- throw new RuntimeException("已經到達隊列頂部");
- }
- }
- }
對比線性表和鏈式表
前面已經說過順序表和鏈式表各自的特點,這里在重申一遍
數組操作優點:1.通過指針快速定位到下標,查詢快速
缺點: 1.數組聲明時即需要確定數組大小。當操作中超過容量時,則需要重新聲明數組,並且復制當前所有數據
2.當需要在中間進行插入或者刪除時,則需要移動大量元素(size-index個)
鏈表操作優點:1.因為每個結點記錄下個結點的引用,則在進行插入和刪除操作時,只需要改變對應下標下結點的引用即可
缺點:1.要得到某個下標的數據,不能通過下標直接得到,需要遍歷整個鏈表。
現在,我們通過進行增刪改查操作來感受一次其效率的差異
思路:通過兩個表,各進行大數據量操作(3W)條數據的操作,記錄操作前系統時間,操作后系統時間,得出操作時間
實現代碼如下
- package cn.javamzd.collection.List;
- public class Test {
- /**
- * @param args
- */
- public static void main(String[] args) {
- //測試自己實現的ArrayList類和Linkedlist類添加30000個數據所需要的時間
- ArrayList<String> al = new ArrayList<String>();
- LinkedList<String> ll = new LinkedList<String>();
- Long aBeginTime=System.currentTimeMillis();//記錄BeginTime
- for(int i=0;i<30000;i++){
- al.add("now"+i);
- }
- Long aEndTime=System.currentTimeMillis();//記錄EndTime
- System.out.println("arrylist add time--->"+(aEndTime-aBeginTime));
- Long lBeginTime=System.currentTimeMillis();//記錄BeginTime
- for(int i=0;i<30000;i++){
- ll.add("now"+i);
- }
- Long lEndTime=System.currentTimeMillis();//記錄EndTime
- System.out.println("linkedList add time---->"+(lEndTime-lBeginTime));
- //測試JDK提供的ArrayList類和LinkedList類添加30000個數據所需要的世界
- java.util.ArrayList<String> sal=new java.util.ArrayList<String>();
- java.util.LinkedList<String> sll=new java.util.LinkedList<String>();
- Long saBeginTime=System.currentTimeMillis();//記錄BeginTime
- for(int i=0;i<30000;i++){
- sal.add("now"+i);
- }
- Long saEndTime=System.currentTimeMillis();//記錄EndTime
- System.out.println("JDK arrylist add time--->"+(saEndTime-saBeginTime));
- Long slBeginTime=System.currentTimeMillis();//記錄BeginTime
- for(int i=0;i<30000;i++){
- sll.add("now"+i);
- }
- Long slEndTime=System.currentTimeMillis();//記錄EndTime
- System.out.println("JDK linkedList add time---->"+(slEndTime-slBeginTime));
- }
- }
得到測試結果如下:
arrylist add time--->446 linkedList add time---->9767 JDK arrylist add time--->13 JDK linkedList add time---->12 |
由以上數據,我們可知:
1.JDK中的ArrayList何LinkedList在添加數據時的性能,其實幾乎是沒有差異的
2.我們自己寫的List的性能和JDK提供的List的性能還是存在巨大差異的
3.我們使用鏈表添加操作,花費的時間是巨大的,比ArrayList都大幾十倍
第三條顯然是跟我們最初的設計不相符的,按照我們最初的設想,鏈表的添加應該比順序表更省時
查看我們寫的源碼,可以發現:
我們每次添加一個數據時,都需要遍歷整個表,得到表尾,再在表尾添加,這是很不科學的
現改進如下:設立一個Node<E>類的成員變量end來指示表尾,這樣每次添加時,就不需要再重新遍歷得到表尾
改進后add()方法如下
- public boolean add(E e) {
- if (size == 0) {
- header.e = e;
- } else {
- // 根據需要添加的內容,封裝為結點
- Node<E> newNode = new Node<E>(e);
- //在表尾添加元素
- last.addNext(newNode);
- //將表尾指向當前最后一個元素
- last = newNode;
- }
- size++;// 當前大小自增加1
- return true;
- }
ArrayList添加的效率和JDK中對比起來也太低
分析原因為:
每次擴大容量時,擴大量太小,需要進行的復制操作太多
現在改進如下:
每次擴大,則擴大容量為當前的三倍,此改進僅需要更改ensureCapacity()方法中的一行代碼,此處就不列出了。
改進后,再次運行添加元素測試代碼,結果如下:
arrylist add time--->16 linkedList add time---->8 JDK arrylist add time--->7 JDK linkedList add time---->7 |
雖然還有改進的空間,但是顯然,我們的效果已經大幅度改進了,而且也比較接近JDK了
接下來測試插入操作的效率
我們只需要將測試代碼中的添加方法(add())改成插入方法(insert(int index,E e)),為了使插入次數盡可能多,我們把index都設置為0
測試結果如下:
arrylist inset time--->17 linkedList inset time---->13 JDK arrylist inset time--->503 JDK linkedList inset time---->11 |
多次測試,發現我們寫的ArrayList在插入方法的效率都已經超過JDK了,而且也接近LinkedLst了。撒花!!!