資料結構一線性表 (順序表、單鏈表、雙鏈表)
1、線性表及其邏輯結構
線性表是最簡單也是最常用的一種資料結構。英文字母表(A、B、…、Z)是一個線性表,表中每個英文字母是一個數據元素;成績單是一個線性表,表中每一行是一個數據元素,每個資料元素又由學號、姓名、成績等資料項組成。
1.1 線性表的定義
線性表是具有相同特性的資料元素的一個有限序列。線性表一般表示為:
L = (a1, a2, …, ai,ai+1 ,…, an)
線性表中元素在位置上是有序的,這種位置上有序性就是一種線性關係,用二元組表示:
L = (D, R)
D = {ai| 1≤i≤n, n≥0}
R = {r}
r = {<ai, ai+1> | 1≤i≤n-1}
1.2 線性表的抽象資料型別描述
將線性表資料結構抽象成為一種資料型別,這個資料型別中包含資料元素、元素之間的關係、操作元素的基本演算法。對於基本資料型別(int、float、boolean等等)java已經幫我們實現了用於操作他們的基本運算,我們需要基於這些基本運算,為我們封裝的自定義資料型別提供操作它們的演算法。比如陣列就是一種被抽象出來的線性表資料型別,陣列自帶很多基本方法用於操作資料元素。
Java中的List
我們經常會使用到,但是很少關注其內部實現,List
ArrayList
是對線性表順序儲存結構的實現,LinkedList
是線性錶鏈式儲存結構的實現等。儲存結構沒有確定我們就不知道資料怎麼儲存,但是對於線性表這種邏輯結構中資料的基本操作我們可以預知,無非就是獲取長度、獲取指定位置的資料、插入資料、刪除資料等等操作,可以參考List
。
對於本系列文章,只是對資料結構和一些常用演算法學習,接下來的程式碼將選擇性實現並分析演算法。對線性表的抽象資料型別描述如下:
/**
* autour : openXu
* date : 2018/7/13 15:41
* className : IList
* version : 1.0
* description : 線性表的抽象資料型別
*/
public interface IList<T> {
/**
* 判斷線性表是否為空
* @return
*/
boolean isEmpty();
/**
* 獲取長度
* @return
*/
int length();
/**
* 將結點新增到指定序列的位置
* @param index
* @param data
* @return
*/
boolean add(int index, T data);
/**
* 將指定的元素追加到列表的末尾
* @param data
* @return
*/
boolean add(T data);
/**
* 根據index移除元素
* @param index
* @return
*/
T remove(int index);
/**
* 移除值為data的第一個結點
* @param data
* @return
*/
boolean remove(T data);
/**
* 移除所有值為data的結點
* @param data
* @return
*/
boolean removeAll(T data);
/**
* 清空表
*/
void clear();
/**
* 設定指定序列元素的值
* @param index
* @param data
* @return
*/
T set(int index, T data);
/**
* 是否包含值為data的結點
* @param data
* @return
*/
boolean contains(T data);
/**
* 根據值查詢索引
* @param data
* @return
*/
int indexOf(T data);
/**
* 根據data值查詢最後一次出現在表中的索引
* @param data
* @return
*/
int lastIndexOf(T data);
/**
* 獲取指定序列的元素
* @param index
* @return
*/
T get(int index);
/**
* 輸出格式
* @return
*/
String toString();
}
2、線性表的順序儲存結構
2.1 順序表
把線性表中的所有元素按照其邏輯順序依次儲存在計算機儲存器中指定儲存位置開始的一塊連續的儲存空間中。在Java中建立一個數組物件就是分配了一塊可供使用者使用的連續的儲存空間,該儲存空間的起始位置就是由陣列名錶示的地址常量。線性表的順序儲存結構是利用陣列來實現的。
在Java中,我們通常利用下面的方式來使用陣列:
int[] array = new int[]{1,2,3}; //建立一個數組
Array.getInt(array, 0); //獲取陣列中序列為0的元素
Array.set(array, 0, 1); //設定序列為0的元素值為1
這種方式建立的陣列是固定長度的,其容量無法修改,當array被創建出來的時候,系統只為其分配3個儲存空間,所以我們無法對其進行新增和刪除操作。Array
這個類裡面提供了很多方法用於運算元組,這些方法都是靜態的,所以Array
是一個用於運算元組的工具類,這個類提供的方法只有兩種:get和set,所以只能獲取和設定陣列中的元素,然後對於這兩種操作,我們通常使用array[i]、array[i] = 0的簡化方式,所以Array這個類用的比較少。
另外一種陣列ArrayList
,其內部維護了一個數組,所以本質上也是陣列,其操作都是對陣列的操作,與上述陣列不同的是,ArrayList是一種可變長度的陣列。既然陣列建立時就已經分配了儲存空間,為什麼ArrayList是長度可變的呢?長度可變意味著可以從陣列中新增、刪除元素,向ArrayList中新增資料時,實際上是建立了一個新的陣列,將原陣列中元素一個個複製到新陣列後,將新元素新增進來。如果ArrayList僅僅做了這麼簡單的操作,那他就不應該出現了。ArrayList中的陣列長度是大於等於其元素個數的,當執行add()操作時首先會檢查陣列長度是否夠用,只有當陣列長度不夠用時才會建立新的陣列,由於建立新陣列意味著老資料的搬遷,所以這個機制也算是利用空間換取時間上的效率。但是如果新增操作並不是尾部新增,而是頭部或者中間位置插入,也避免不了元素位置移動。
2.2 順序表基本運算的實現
/**
* autour : openXu
* date : 2018/7/11 10:45
* className : LinearArray
* version : 1.0
* description : 線性表的順序儲存結構(順序表),是由陣列來實現的
*/
public class LinearArray<T> implements IList<T>{
private Object[] datas;
/**
* 通過給定的陣列 建立順序表
* @param objs
* @return
*/
public static <T> LinearArray<T> createArray(T[] objs){
LinearArray<T> array = new LinearArray();
array.datas = new Object[objs.length];
for(int i = 0; i<objs.length; i++)
array.datas[i] = objs[i];
return array;
}
private LinearArray(){
}
@Override
public boolean isEmpty() {
return datas.length == 0;
}
@Override
public int length() {
return datas.length;
}
/**
* 獲取指定位置的元素
* 分析:時間複雜度O(1)
* 從順序表中檢索值是簡單高效的,因為順序表內部採用陣列作為容器,陣列可直接通過索引值訪問元素
*/
@Override
public T get(int index) {
if (index<0 || index >= datas.length)
throw new IndexOutOfBoundsException();
return (T) datas[index];
}
/**
* 為指定索引的結點設定值
* 分析:時間複雜度O(1)
*/
@Override
public T set(int index, T data) {
if (index<0 || index >= datas.length)
throw new IndexOutOfBoundsException();
T oldValue = (T) datas[index];
datas[index] = data;
return oldValue;
}
/**
* 判斷是否包含某值只需要判斷該值有沒有出現過
* 分析:時間複雜度O(n)
*/
@Override
public boolean contains(T data) {
return indexOf(data) >= 0;
}
/**
* 獲取某值第一次出現的索引
* 分析:時間複雜度O(n)
*/
@Override
public int indexOf(T data) {
if (data == null) {
for (int i = 0; i < datas.length; i++)
if (datas[i]==null)
return i;
} else {
for (int i = 0; i < datas.length; i++)
if (data.equals(datas[i]))
return i;
}
return -1;
}
/**
* 獲取某值最後一次出現的索引
* 分析:時間複雜度O(n)
*/
@Override
public int lastIndexOf(T data) {
if (data == null) {
for (int i = datas.length-1; i >= 0; i--)
if (datas[i]==null)
return i;
} else {
for (int i = datas.length-1; i >= 0; i--)
if (data.equals(datas[i]))
return i;
}
return -1;
}
/**
* 指定位置插入元素
* 分析:時間複雜度O(n)
* 在陣列中插入元素時,需要建立一個比原陣列容量大1的新陣列,
* 將原陣列中(0,index-1)位置的元素拷貝到新陣列,指定新陣列index位置元素值為新值,
* 繼續將原陣列(index, length-1)的元素拷貝到新陣列
* @param index
* @param data
* @return
*/
@Override
public boolean add(int index, T data) {
if (index > datas.length || index < 0)
throw new IndexOutOfBoundsException();
Object destination[] = new Object[datas.length + 1];
System.arraycopy(datas, 0, destination, 0, index);
destination[index] = data;
System.arraycopy(datas, index, destination, index
+ 1, datas.length - index);
datas = destination;
return true;
}
/**
* 在順序表末尾處插入元素
* 分析:時間複雜度O(n)
* 同上面一樣,也需要建立新陣列
* @param data
* @return
*/
@Override
public boolean add(T data) {
Object destination[] = new Object[datas.length + 1];
System.arraycopy(datas, 0, destination, 0, datas.length);
destination[datas.length] = data;
datas = destination;
return true;
}
/**
* 有序表新增元素
* @param data
* @return
*/
public boolean addByOrder(int data) {
int index = 0;
//找到順序表中第一個大於等於data的元素
while(index<datas.length && (int)datas[index]<data)
index++;
if((int)datas[index] == data) //不能有相同元素
return false;
Object destination[] = new Object[datas.length + 1];
System.arraycopy(datas, 0, destination, 0, index);
//將datas[index]及後面元素後移一位
System.arraycopy(datas, index, destination, index+1, datas.length-index);
destination[index] = data;
datas = destination;
return true;
}
/**
* 移除指定索引的元素
* 分析:時間複雜度O(n)
* 此處由於陣列元素數量-1,所以需要建立新陣列。
* ArrayList由於是動態陣列(list.size()≠data.length),所以只需要將刪除的元素之後的前移一位
* @param index
* @return
*/
@Override
public T remove(int index) {
if (index >= datas.length || index < 0)
throw new IndexOutOfBoundsException();
T oldValue = (T) datas[index];
fastRemove(index);
return oldValue;
}
/**
* 刪除指定值的第一個元素
* @param data
* @return
*/
@Override
public boolean remove(T data) {
if (data == null) {
for (int index = 0; index < datas.length; index++)
if (datas[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < datas.length; index++)
if (data.equals(datas[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/**
* 移除指定序列的元素
* @param index
*/
private void fastRemove(int index) {
Object destination[] = new Object[datas.length - 1];
System.arraycopy(datas, 0, destination, 0, index);
System.arraycopy(datas, index+1, destination, index,
datas.length - index-1);
datas = destination;
}
@Override
public boolean removeAll(T data) {
return false;
}
@Override
public void clear() {
datas = new Object[]{};
}
@Override
public String toString() {
if(isEmpty())
return "";
String str = "[";
for(int i = 0; i<datas.length; i++){
str += (datas[i]+", ");
}
str = str.substring(0, str.lastIndexOf(", "));
return str+"]";
}
}
演算法分析:
插入元素:
刪除元素:
3、線性表的鏈式儲存結構
順序表必須佔用一整塊事先分配大小固定的儲存空間,這樣不便於儲存空間的管理。為此提出了可以實現儲存空間動態管理的鏈式儲存方式–連結串列。
3.1 連結串列
在鏈式儲存中,每個儲存結點不僅包含元素本身的資訊(資料域),還包含元素之間邏輯關係的資訊,即一個結點中包含有直接後繼結點的地址資訊,這稱為指標域。這樣可以通過一個結點的指標域方便的找到後繼結點的位置。
由於順序表中每個元素至多隻有一個直接前驅元素和一個直接後繼元素。當採用鏈式儲存時,一種最簡單也最常用的方法是:
在每個結點中除包含資料域外,只設置一個指標域用以指向其直接後繼結點,這種構成的連結表稱為線性單向連結表,簡稱單鏈表。
另一種方法是,在每個結點中除包含數值域外,設定兩個指標域,分別用以指向直接前驅結點和直接後繼結點,這樣構成的連結表稱為線性雙向連結表,簡稱雙鏈表。
單鏈表當訪問一個結點後,只能接著訪問它的直接後繼結點,而無法訪問他的直接前驅結點。雙鏈表則既可以依次向後訪問每個結點,也可以依次向前訪問每個結點。
單鏈表結點元素型別定義:
public class LNode {
protected LNode next; //指標域,指向直接後繼結點
protected Object data; //資料域
}
雙鏈表結點元素型別定義:
public class DNode {
protected DNode prior; //指標域,指向直接前驅結點
protected DNode next; //指標域,指向直接後繼結點
protected Object data; //資料域
}
在順序表中,邏輯上相鄰的元素,對應的儲存位置也相鄰,所以進行插入或刪除操作時,通常需要平均移動半個表的元素,這是相當費時的操作。
在連結串列中,每個結點儲存位置可以任意安排,不必要求相鄰,插入或刪除操作只需要修改相關結點的指標域即可,方便省時。對於單鏈表,如果要在結點p之前插入一個新結點,由於通過p並不能找到其前驅結點,我們需要從連結串列表頭遍歷至p的前驅結點然後進行插入操作,這樣時間複雜度就是O(n),而順序表插入刪除結點時間複雜度也是O(n),那為什麼說連結串列插入刪除操作更加高效呢?因為單鏈表插入刪除操作所消耗的時間主要在於查詢前驅結點,這個查詢工作的時間複雜度為O(n),而真正超如刪除時間為O(1),還有順序表需要移動結點,移動結點通常比單純的查詢更加費時,連結串列不需要連續的空間,不需要擴容建立新表,所以同樣時間複雜度O(n),連結串列更適合插入和刪除操作。對於遍歷查詢前驅結點的問題,在雙鏈表中就能很好的解決,雙鏈表在已知某結點的插入和刪除操作時間複雜度是O(1)。
由於連結串列的每個結點帶有指標域,從儲存密度來講,這是不經濟的。所謂儲存密度是指結點資料本身所佔儲存量和整改結點結構所佔儲存量之比。
3.2 單鏈表基本運算的實現
/**
* autour : openXu
* date : 2018/7/11 15:48
* className : LinkList
* version : 1.0
* description : 單鏈表基本實現
*/
public class LinkList<T> implements IList<T>{
public LNode<T> head; //單鏈表開始結點
/**
* 1.1 建立單鏈表(頭插法:倒序)
* 解:遍歷陣列,建立新結點,新結點的指標域指向頭結點,讓新結點作為頭結點
* 時間複雜度O(n)
* @param array
* @return
*/
public static <T> LinkList<T> createListF(T[] array){
LinkList llist = new LinkList();
if(array!=null && array.length>0) {
for (T obj : array) {
LNode<T> node = new LNode();
node.data = obj;
node.next = llist.head;
llist.head = node;
}
}
return llist;
}
/**
* 1.2 建立單鏈表(尾插法:順序)
* 解:
* 時間複雜度O(n)
* @param array
* @return
*/
public static <T> LinkList<T> createListR(T[] array){
LinkList llist = new LinkList();
if(array!=null && array.length>0){
llist.head = new LNode();
llist.head.data = array[0];
LNode<T> temp = llist.head;
for(int i = 1; i < array.length; i++){
LNode node = new LNode();
node.data = array[i];
temp.next = node;
temp = node;
}
}
return llist;
}
/**
* 判斷單鏈表是否為空表
* 時間複雜度O(1)
* @return
*/
@Override
public boolean isEmpty() {
return head==null;
}
/**
* 4 獲取單鏈表長度
* 時間複雜度O(n)
* @return
*/
@Override
public int length() {
if(head==null)
return 0;
int l = 1;
LNode node = head;
while(node.next!=null) {
l++;
node = node.next;
}
return l;
}
@Override
public void clear() {
head = null;
}
@Override
public T set(int index, T data) {
return null;
}
@Override
public boolean contains(T data) {
return false;
}
@Override
public T get(int index) {
return getNode(index).data;
}
/**
* 6.1 獲取指定索引的結點
* 時間複雜度O(n)
* @param index
* @return
*/
public LNode<T> getNode(int index){
LNode node = head;
int j = 0;
while(j < index && node!=null){
j++;
node = node.next;
}
return node;
}
/**
* 6.2 獲取指定資料值結點的索引
* 時間複雜度O(n) 空間複雜度O(1)
* @param data
* @return
*/
@Override
public int indexOf(T data) {
if(head==null)
return -1; //沒有此結點
LNode node = head;
int j = 0;
while(node!=null){
if(node.data.equals(data))
return j;
j++;
node = node.next;
}
return -1;
}
@Override
public int lastIndexOf(T data) {
if(head==null)
return -1;
int index = -1;
LNode node = head;
int j = 0;
while(node!=null){
if(node.data.equals(data)) {
index = j;
}
j++;
node = node.next;
}
return index;
}
/**
* 6.3 單鏈表中的倒數第k個結點(k > 0)
* 解:先找到順數第k個結點,然後使用前後指標移動到結尾即可
* 時間複雜度O(n) 空間複雜度O(1)
* @param k
* @return
*/
public LNode<T> getReNode(int k){
if(head==null)
return null;
int len = length();
if(k > len)
return null;
LNode target = head;
LNode next = head;
for(int i=0;i < k;i++)
next = next.next;
while(next!=null){
target = target.next;
next = next.next;
}
return target;
}
/**
* 6.4 查詢單鏈表的中間結點
* 時間複雜度O(n) 空間複雜度O(1)
* @return
*/
public LNode getMiddleNode(){
if(head == null|| head.next == null)
return head;
LNode target = head;
LNode temp = head;
while(temp != null && temp.next != null){
target = target.next;
temp = temp.next.next;
}
return target;
}
/**
* 2.1 將單鏈表合併為一個單鏈表
* 解:遍歷第一個表,用其尾結點指向第二個表頭結點
* 時間複雜度O(n)
* @return
*/
public static LNode mergeList(LNode head1, LNode head2){
if(head1==null) return head2;
if(head2==null) return head1;
LNode loop = head1;
while(loop.next!=null) //找到list1尾結點
loop = loop.next;
loop.next = head2; //將list1尾結點指向list2頭結點
return head1;
}
/**
* 2.1 通過遞迴,合併兩個有序的單鏈表head1和head2
*
* 解:兩個指標分別指向兩個頭結點,比較兩個結點大小,
* 小的結點指向下一次比較結果(兩者中較小),最終返回第一次遞迴的最小結點
* @param head1
* @param head2
* @return
*/
public static LNode mergeSortedListRec(LNode head1, LNode head2){
if(head1==null)return head2;
if(head2==null)return head1;
if (((int)head1.data)>((int)head2.data)) {
head2.next = mergeSortedListRec(head2.next, head1);
return head2;
} else {
head1.next = mergeSortedListRec(head1.next, head2);
return head1;
}
}
/**
* 3.1 迴圈的方式將單鏈表反轉
* 時間複雜度O(n) 空間複雜度O(1)
*/
public void reverseListByLoop() {
if (head == null || head.next == null)
return;
LNode pre = null;
LNode nex = null;
while (head != null) {
nex = head.next;
head.next = pre;
pre = head;
head = nex;
}
head = pre;
}
/**
* 3.2 遞迴的方式將單鏈表反轉,返回反轉後的連結串列頭結點
* 時間複雜度O(n) 空間複雜度O(n)
*/
public LNode reverseListByRec(LNode head) {
if(head==null||head.next==null)
return head;
LNode reHead = reverseListByRec(head.next);
head.next.next = head;
head.next = null;
return reHead;
}
/**
* 5.1 獲取單鏈表字符串表示
* 時間複雜度O(n)
*/
@Override
public String toString() {
if(head == null)
return "";
LNode node = head;
StringBuffer buffer = new StringBuffer();
while(node != null){
buffer.append(node.data+" -> ");
node = node.next;
}
return buffer.toString();
}
public static String display(LNode head){
if(head == null)
return "";
LNode node = head;
StringBuffer buffer = new StringBuffer();
while(node != null){
buffer.append(" -> "+node.data);
node = node.next;
}
return buffer.toString();
}
/**
* 5.2 用棧的方式獲取單鏈表從尾到頭倒敘字串表示
* 解:由於棧具有先進後出的特性,現將表中的元素放入棧中,然後取出就倒序了
* 時間複雜度O(n) 空間複雜度O(1)
* @return
*/
public String displayReverseStack(){
if(head == null)
return "";
Stack <LNode> stack = new Stack < >(); //堆疊 先進先出
LNode head = this.head;
while(head!=null){
stack.push(head);
head=head.next;
}
StringBuffer buffer = new StringBuffer();
while(!stack.isEmpty()){
//pop()移除堆疊頂部的物件,並將該物件作為該函式的值返回。
buffer.append(" -> "+stack.pop().data);
}
return buffer.toString();
}
/**
* 5.3 用遞迴的方式獲取單鏈表從尾到頭倒敘字串表示
* @return
*/
public void displayReverseRec(StringBuffer buffer, LNode head){
if(head==null)
return;
displayReverseRec(buffer, head.next);
buffer.append(" -> ");
buffer.append(head.data);
}
/**
* 7.1 插入結點
* 解:先找到第i-1個結點,讓建立的新結點的指標域指向第i-1結點指標域指向的結點,
* 然後將i-1結點的指標域指向新結點
* 時間複雜度O(n) 空間複雜度O(1)
* @param data
* @param index
*/
@Override
public boolean add(int index, T data) {
if(index==0){ //插入為頭結點
LNode temp = new LNode();
temp.next = head;
return true;
}
int j = 0;
LNode node = head;
while(j < index-1 && node!=null){ //找到序列號為index-1的結點
j++;
node = node.next;
}
if(node==null)
return false;
LNode temp = new LNode(); //建立新結點
temp.data = data;
temp.next = node.next; //新結點插入到Index-1結點之後
node.next = temp;
return true;
}
@Override
public boolean add(T data) {
LNode node = head;
while(node!=null && node.next!=null) //找到尾結點
node = node.next;
LNode temp = new LNode(); //建立新結點
temp.data = data;
node.next = temp;
return false;
}
@Override
public T remove(int index) {
LNode<T> node = deleteNode(index);
return node==null?null:node.data;
}
/**
* 7.2 刪除結點
* 解:讓被刪除的結點前一個結點的指標域指向後一個結點指標域
* 時間複雜度O(n) 空間複雜度O(1)
* @return
*/
public LNode deleteNode(int index){
LNode node = head;
if(index==0){ //刪除頭結點
if(node==null)
return null;
head = node.next;
return node;
}
//非頭結點
int j = 0;
while(j < index-1 && node!=null){ //找到序列號為index-1的結點
j++;
node = node.next;
}
if(node==null)
return null;
LNode delete = node.next;
if(delete==null)
return null; //不存在第index個結點
node.next = delete.next;
return delete;
}
@Override
public boolean remove(T data) {
return false;
}
@Override
public boolean removeAll(T data) {
return false;
}
/**
* 7.3 給出一單鏈表頭指標head和一節點指標delete,要求O(1)時間複雜度刪除節點delete
* 解:將delete節點value值與它下個節點的值互換的方法,
* 但是如果delete是最後一個節點,需要特殊處理,但是總得複雜度還是O(1)
* @return
*/
public static void deleteNode(LNode head, LNode delete){
if(delete==null)
return;
//首先處理delete節點為最後一個節點的情況
if(delete.next==null){
if(head==delete) //只有一個結點
head = null;
else{
//刪除尾結點
LNode temp = head;
while(temp.next!=delete)
temp = temp.next;
temp.next=null;
}
} else{
delete.data = delete.next.data;
delete.next = delete.next.next;
}
return;
}
/**
* 8.1 判斷一個單鏈表中是否有環
* 解:使用快慢指標方法,如果存在環,兩個指標必定指向同一結點
* 時間複雜度O(n) 空間複雜度O(1)
* @return
*/
public static boolean hasCycle(LNode head){
LNode p1 = head;
LNode p2 = head;
while(p1!=null && p2!=null){
p1 = p1.next; //一次跳一步
p2 = p2.next.next; //一次跳兩步
if(p2 == p1)
return true;
}
return false;
}
/**
* 8.2、已知一個單鏈表中存在環,求進入環中的第一個節點
* 利用hashmap,不要用ArrayList,因為判斷ArrayList是否包含某個元素的效率不高
* @param head
* @return
*/
public static LNode getFirstNodeInCycleHashMap(LNode head){
LNode target = null;
HashMap<LNode,Boolean > map=new HashMap< >();
while(head != null){
if(map.containsKey(head)) {
target = head;
break;
} else {
map.put(head, true);
head = head.next;
}
}
return target;
}
/**
* 8.3、已知一個單鏈表中存在環,求進入環中的第一個節點,不用hashmap
* 用快慢指標,與判斷一個單鏈表中是否有環一樣,找到快慢指標第一次相交的節點,
* 此時這個節點距離環開始節點的長度和連結串列頭距離環開始的節點的長度相等
* 參考 https://www.cnblogs.com/fankongkong/p/7007869.html
* @param head
* @return
*/
public static LNode getFirstNodeInCycle(LNode head){
LNode fast = head;
LNode slow = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast)
break;
}
if(fast == null||fast.next == null)
return null;//判斷是否包含環
//相遇節點距離環開始節點的長度和連結串列投距離環開始的節點的長度相等
slow=head;
while(slow!=fast){
slow=slow.next;
fast=fast.next;
}//同步走
return slow;
}
/**
* 9、判斷兩個單鏈表是否相交,如果相交返回第一個節點,否則返回null
* ①、暴力遍歷兩個表,是否有相同的結點(時間複雜度O(n²))
* ②、第一個表的尾結點指向第二個表頭結點,然後判斷第二個表是否存在環,但不容易找出交點(時間複雜度O(n))
* ③、兩個連結串列相交,必然會經過同一個結點,這個結點的後繼結點也是相同的(連結串列結點只有一個指標域,後繼結點只能有一個),
* 所以他們的尾結點必然相同。兩個連結串列相交,只能是前面的結點不同,所以,砍掉較長連結串列的差值後同步遍歷,判斷結點是否相同,相同的就是交點了。
* 時間複雜度(時間複雜度O(n))
* @param list1
* @param list2
* @return 交點
*/
public static LNode isIntersect(LinkList list1, LinkList list2){
LNode head1 = list1.head;
LNode head2 = list2.head;
if(head1==null || head2==null)return null;
int len1 = list1.length();
int len2 = list2.length();
//砍掉較長連結串列的差值
if(len1 >= len2){
for(int i=0;i < len1-len2;i++){
head1=head1.next;
}
}else{
for(int i=0;i < len2-len1;i++){
head2=head2.next;
}
}
//同步遍歷
while(head1 != null&&head2 != null){
if(head1 == head2)
return head1;
head1=head1.next;
head2=head2.next;
}
return null;
}
}
演算法分析
判斷一個單鏈表中是否有環(尾結點指向過往結點):
我們可以通過HashMap判斷,遍歷結點,將結點值放入HashMap,如果某一刻發現當前結點在map中已經存在,則存在環,並且此結點正是環的入口,此演算法見8.2方法。但是有一種問法是不通過任何其他資料結構怎麼判斷單鏈表是否存在環。
這樣我們可利用的就只有單鏈表本身,一種解法是通過快慢指標,遍歷連結串列,一個指標跳一步(慢指標步長為1),另一個指標跳兩步(快指標步長為2),如果存在環,這兩個指標必將在某一刻指向同一結點,假設此時慢指標跳了n步,則快指標跳的步數為n/2步:
判斷兩個單鏈表是否相交:
由於單鏈表的特性(只有一個指標域),如果兩個表相交,那必定是Y形相交,不會是X形相交,如圖所示。兩個單鏈表後面的結點相同,不同的部分只有前面,砍掉較長的連結串列的前面部分,然後兩個連結串列同步遍歷,必將指向同一個結點,這個結點就是交點:
3.3 雙鏈表
雙鏈表中每個結點有兩個指標域,一個指向其直接後繼結點,一個指向其直接前驅結點。
建立雙鏈表也有兩種方法,頭插法和尾插法,這與建立單鏈表過程相似。在雙鏈表中,有些演算法如求長度、取元素值、查詢元素等演算法與單鏈表中相應演算法是相同的。但是在單鏈表中,進行結點插入和刪除時涉及前後結點的一個指標域的變化,而雙鏈表中結點的插入和刪除操作涉及前後結點的兩個指標域的變化。java中LinkedList正是對雙鏈表的實現,演算法可參考此類。
雙鏈表基本運算的實現
/**
* autour : openXu
* date : 2018/7/11 15:48
* className : LinkList
* version : 1.0
* description : 雙鏈表基本實現
*/
public class DLinkList<T> implements IList<T>{
transient DNode<T> first; //雙鏈表開始結點
transient DNode<T> last; //雙鏈表末端結點
private int size; //結點數
/**
* 建立單鏈表(頭插法:倒序)
* 時間複雜度O(n)
* @param array
* @return
*/
public static <T> DLinkList<T> createListF(T[] array){
DLinkList dlist = new DLinkList();
if(array!=null && array.length>0) {
dlist.size = array.length;
for (T obj : array) {
DNode<T> node = new DNode();
node.data = obj;
node.next = dlist.first;
if(dlist.first!=null)
dlist.first.prior = node; //相比單鏈表多了此步
else
dlist.last = node;
dlist.first = node;
}
}
return dlist;
}
/**
* 1.2 建立單鏈表(尾插法:順序)
* 時間複雜度O(n)
* @param array
* @return
*/
public static <T> DLinkList<T> createListR(T[] array){
DLinkList dlist = new DLinkList();
if(array!=null && array.length>0){
dlist.size = array.length;
dlist.first = new DNode<T>();
dlist.first.data = array[0];
dlist.last = dlist.first;
for(int i = 1; i < array.length; i++){
DNode<T> node = new DNode();
node.data = array[i];
dlist.last.next = node;
node.prior = dlist.last; //相比單鏈表多了此步
dlist.last = node;
}
}
return dlist;
}
@Override
public boolean isEmpty() {
return size==0;
}
@Override
public int length() {
return size;
}
/**2 新增結點*/
@Override
public boolean add(int index, T data) {
if(index < 0 || index > size)
throw new IndexOutOfBoundsException();
DNode<T> newNode = new DNode();
newNode.data = data;
if (index == size) { //在末尾新增結點不需要遍歷
final DNode<T> l = last;
if (l == null) //空表
first = newNode;
else {
l.next = newNode;
newNode.prior = l;
}
last = newNode;
size++;
} else {
//其他位置新增結點需要遍歷找到index位置的結點
DNode<T> indexNode = getNode(index);
DNode<T> pred = indexNode.prior;
newNode.prior = pred;
newNode.next = indexNode;
indexNode.prior = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
}
return false;
}
@Override
public boolean add(T data) {
return add(size, data);
}
/**3 刪除結點*/
@Override
public T remove(int index) {
if(index < 0 || index >= size)
throw new IndexOutOfBoundsException();
return unlink(getNode(index));
}
@Override
public boolean remove(T data) {
if (data == null) {
for (DNode<T> x = first; x != null; x = x.next) {
if (x.data == null) {
unlink(x);
return true;
}
}
} else {
for (DNode<T> x = first; x != null; x =