原創不易,如需轉載,請注明出處https://www.cnblogs.com/baixianlong/p/10759599.html,否則將追究法律責任!!!
一、鏈表介紹
1、什么是鏈表?
- 鏈表是一種物理存儲結構上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。如下圖所示,在數據結構中,a1里面的指針存儲着a2的地址,這樣一個鏈接一個,就形成了鏈表。
- 相鄰元素之間通過指針鏈接
- 最后一個元素的后繼指針為NULL
- 在程序執行過程中,鏈表的長度可以增加或縮小
- 鏈表的空間能夠按需分配
- 沒有內存空間的浪費
2、鏈表的優缺點?
-
優點:
- 插入和刪除時不需移動其他元素, 只需改變指針,效率高。
- 鏈表各個節點在內存中空間不要求連續,空間利用率高。
- 大小沒有固定,拓展很靈活。
-
缺點:
- 查找數據時效率低,因為不具有隨機訪問性。
3、鏈表的種類?
- 有單鏈表、雙向鏈表、循環單鏈表、循環雙鏈表等等。
二、單鏈的實現和相關操作
1、鏈表類的創建(以下均已單鏈表為基准)
public class SingleLinkedList {
//head為頭節點,他不存放任何的數據,只是充當一個指向鏈表中真正存放數據的第一個節點的作用
public Node head = new Node();
//內部類,定義node節點,使用內部類的最大好處是可以和外部類進行私有操作的互相訪問
class Node{
public int val; //int類型會導致head節點的val為0,不影響我們學習
public Node next;
public Node(){}
public Node(int val){
this.val = val;
}
}
//下面就可以自定義各種鏈表操作。。。
}
2、鏈表添加結點
//找到鏈表的末尾結點,把新添加的數據作為末尾結點的后續結點
public void add(int data){
if (head.next == null){
head.next = new Node(data);
return;
}
Node temp = head;
while (temp.next != null){
temp = temp.next;
}
temp.next = new Node(data);
}
3、鏈表刪除節點
//把要刪除結點的前結點指向要刪除結點的后結點,即直接跳過待刪除結點
public boolean deleteNode(int index){
if (index < 0 || index > length() ){
return false;
}
if (index == 1){ //刪除頭結點
head = head.next;
return true;
}
Node preNode = head;
Node curNode = preNode.next;
int i = 2;
while (curNode!=null){
if (index == i){
preNode.next = curNode.next; //指向刪除節點的后一個節點
break;
}
preNode = curNode;
curNode = preNode.next;
i++;
}
return true;
}
4、鏈表長度、節點獲取以及鏈表遍歷
//獲取鏈表長度
public int length(){
int length = 0;
Node temp = head;
while (temp.next!=null){
length++;
temp = temp.next;
}
return length;
}
//獲取最后一個節點
public Node getLastNode(){
Node temp = head;
while (temp.next != null){
temp = temp.next;
}
return temp;
}
//獲取第index節點
public Node getNodeByIndex(int index){
if(index<1 || index>length()){
return null;
}
Node temp = head;
int i = 1;
while (temp.next != null){
temp = temp.next;
if (index==i){
break;
}
i++;
}
return temp;
}
//打印節點
public void printLink(){
Node curNode = head;
while(curNode !=null){
System.out.print(curNode.val+" ");
curNode = curNode.next;
}
}
5、查找單鏈表中的倒數第n個結點
//兩個指針,第一個指針向前移動k-1次,之后兩個指針共同前進,當前面的指針到達末尾時,后面的指針所在的位置就是倒數第k個位置
public Node findReverNode(int index){
if(index<1 || index>length()){
return null;
}
Node first = head;
Node second = head;
for (int i = 0; i < index - 1; i++) {
second = second.next;
}
while (second.next != null){
first = first.next;
second = second.next;
}
return first;
}
6、查找單鏈表中的中間結點
//也是設置兩個指針first和second,只不過這里是,兩個指針同時向前走,second指針每次走兩步,
//first指針每次走一步,直到second指針走到最后一個結點時,此時first指針所指的結點就是中間結點。
public Node findMiddleNode(){
Node slowPoint = head;
Node quickPoint = head;
//鏈表結點個數為奇數時,返回的是中間結點;鏈表結點個數為偶數時,返回的是中間兩個結點中的前個
while(quickPoint != null && quickPoint.next != null){
slowPoint = slowPoint.next;
quickPoint = quickPoint.next.next;
}
return slowPoint;
}
7、從尾到頭打印單鏈表
//方法一:先反轉鏈表,再輸出鏈表,需要鏈表遍歷兩次(不建議這么做,改變了鏈表的結構)
。。。
//方法二、通過遞歸來實現(鏈表很長的時候,就會導致方法調用的層級很深,有可能造成StackOverflowError)
public void reservePrt(Node node){
if(node != null){
reservePrt(node.next);
System.out.print(node.val+" ");
}
}
//方法三、把鏈表中的元素放入棧中再輸出,需要維護額外的棧空間
public void reservePrt2(Node node){
if(node != null){
Stack<Node> stack = new Stack<Node>(); //新建一個棧
Node current = head;
//將鏈表的所有結點壓棧
while (current != null) {
stack.push(current); //將當前結點壓棧
current = current.next;
}
//將棧中的結點打印輸出即可
while (stack.size() > 0) {
System.out.print(stack.pop().val+" "); //出棧操作
}
}
}
8、單鏈表的反轉(1->2->3->4變為4->3->2->1)
//從頭到尾遍歷原鏈表,每遍歷一個結點,將其摘下放在新鏈表的最前端。注意鏈表為空和只有一個結點的情況。時間復雜度為O(n)
public void reserveLink(){
Node curNode = head;
Node preNode = null;
while (curNode.next != null){
Node nextNode = curNode.next;
//主要理解以下邏輯
curNode.next = preNode; //將current的下一個結點指向新鏈表的頭結點
preNode = curNode; //將改變了指向的cruNode賦值給preNode
curNode = nextNode;
}
curNode.next = preNode;
preNode = curNode;
head = preNode;
}
9、判斷鏈表是否有環
//設置快指針和慢指針,慢指針每次走一步,快指針每次走兩步,當快指針與慢指針相等時,就說明該鏈表有環
public boolean isRinged(){
if(head == null){
return false;
}
Node slow = head;
Node fast = head;
while(fast.next != null && fast.next.next != null){
slow = slow.next;
fast = fast.next.next;
if(fast == slow){
return true;
}
}
return false;
}
10、取出有環鏈表中,環的長度
//獲取環的相遇點
public Node getFirstMeet(){
if(head == null){
return null;
}
Node slow = head;
Node fast = head;
while(fast.next != null && fast.next.next != null){
slow = slow.next;
fast = fast.next.next;
if(fast == slow){
return slow;
}
}
return null;
}
//首先得到相遇的結點,這個結點肯定是在環里,我們可以讓這個結點對應的指針一直往下走,直到它回到原點,就可以算出環的長度
public int getCycleLength(){
Node current = getFirstMeet(); //獲取相遇點
int length = 0;
while (current != null) {
current = current.next;
length++;
if (current == getFirstMeet()) { //當current結點走到原點的時候
return length;
}
}
return length;
}
11、判斷兩個鏈表是否相交
//兩個鏈表相交,則它們的尾結點一定相同,比較兩個鏈表的尾結點是否相同即可
public boolean isCross(Node head1, Node head2){
Node temp1 = head1;
Node temp2 = head2;
while(temp1.next != null){
temp1 = temp1.next;
}
while(temp2.next != null){
temp2 = temp2.next;
}
if(temp1 == temp2){
return true;
}
return false;
}
12、如果鏈表相交,求鏈表相交的起始點
/**
* 如果鏈表相交,求鏈表相交的起始點:
* 1、首先判斷鏈表是否相交,如果兩個鏈表不相交,則求相交起點沒有意義
* 2、求出兩個鏈表長度之差:len=length1-length2
* 3、讓較長的鏈表先走len步
* 4、然后兩個鏈表同步向前移動,每移動一次就比較它們的結點是否相等,第一個相等的結點即為它們的第一個相交點
*/
public Node findFirstCrossPoint(SingleLinkedList linkedList1, SingleLinkedList linkedList2){
//鏈表不相交
if(!isCross(linkedList1.head,linkedList2.head)){
return null;
}else{
int length1 = linkedList1.length();//鏈表1的長度
int length2 = linkedList2.length();//鏈表2的長度
Node temp1 = linkedList1.head;//鏈表1的頭結點
Node temp2 = linkedList2.head;//鏈表2的頭結點
int len = length1 - length2;//鏈表1和鏈表2的長度差
if(len > 0){//鏈表1比鏈表2長,鏈表1先前移len步
for(int i=0; i<len; i++){
temp1 = temp1.next;
}
}else{//鏈表2比鏈表1長,鏈表2先前移len步
for(int i=0; i<len; i++){
temp2 = temp2.next;
}
}
//鏈表1和鏈表2同時前移,直到找到鏈表1和鏈表2相交的結點
while(temp1 != temp2){
temp1 = temp1.next;
temp2 = temp2.next;
}
return temp1;
}
}
13、合並兩個有序的單鏈表(將1->2->3和1->3->4合並為1->1->2->3->3->4)
//兩個參數代表的是兩個鏈表的頭結點
//方法一
public Node mergeLinkList(Node head1, Node head2) {
if (head1 == null && head2 == null) { //如果兩個鏈表都為空
return null;
}
if (head1 == null) {
return head2;
}
if (head2 == null) {
return head1;
}
Node head; //新鏈表的頭結點
Node current; //current結點指向新鏈表
// 一開始,我們讓current結點指向head1和head2中較小的數據,得到head結點
if (head1.val <= head2.val) {
head = head1;
current = head1;
head1 = head1.next;
} else {
head = head2;
current = head2;
head2 = head2.next;
}
while (head1 != null && head2 != null) {
if (head1.val <= head2.val) {
current.next = head1; //新鏈表中,current指針的下一個結點對應較小的那個數據
current = current.next; //current指針下移
head1 = head1.next;
} else {
current.next = head2;
current = current.next;
head2 = head2.next;
}
}
//合並剩余的元素
if (head1 != null) { //說明鏈表2遍歷完了,是空的
current.next = head1;
}
if (head2 != null) { //說明鏈表1遍歷完了,是空的
current.next = head2;
}
return head;
}
//方法二:遞歸法
public Node merge(Node head1, Node head2) {
if(head1 == null){
return head2;
}
if(head2 == null){
return head1;
}
Node head = null;
if(head1.val <= head2.val){
head = head1;
head.next = merge(head1.next,head2);
}else{
head = head2;
head.next = merge(head1,head2.next);
}
return head;
}
到此單鏈表的一些常見操作展示的差不多了,如有興趣可繼續深入研究~~~
三、其它種類鏈表(拓展)
1、雙向鏈表(java.util中的LinkedList就是雙鏈的一種實現)
雙向鏈表(雙鏈表)是鏈表的一種。和單鏈表一樣,雙鏈表也是由節點組成,它的每個數據結點中都有兩個指針,分別指向直接后繼和直接前驅。所以,從雙向鏈表中的任意一個結點開始,都可以很方便地訪問它的前驅結點和后繼結點。一般我們都構造雙向循環鏈表。
- 優點:對於鏈表中一個給的的結點,可以從兩個方向進行操,雙向鏈表相對單鏈表更適合元素的查詢工作。
- 缺點:
- 每個結點需要再添加一個額外的指針,因此需要更多的空間開銷。
- 結點的插入或者刪除更加費時。
以下是雙鏈的相關實現和操作(其實單鏈弄明白了,雙鏈只不過多維護了個前節點)
public class DoubleLink<T> {
// 表頭
private DNode<T> mHead;
// 節點個數
private int mCount;
// 雙向鏈表“節點”對應的結構體
private class DNode<T> {
public DNode prev;
public DNode next;
public T value;
public DNode(T value, DNode prev, DNode next) {
this.value = value;
this.prev = prev;
this.next = next;
}
}
// 構造函數
public DoubleLink() {
// 創建“表頭”。注意:表頭沒有存儲數據!
mHead = new DNode<T>(null, null, null);
mHead.prev = mHead.next = mHead;
// 初始化“節點個數”為0
mCount = 0;
}
// 返回節點數目
public int size() {
return mCount;
}
// 返回鏈表是否為空
public boolean isEmpty() {
return mCount==0;
}
// 獲取第index位置的節點
private DNode<T> getNode(int index) {
if (index<0 || index>=mCount)
throw new IndexOutOfBoundsException();
// 正向查找
if (index <= mCount/2) {
DNode<T> node = mHead.next;
for (int i=0; i<index; i++)
node = node.next;
return node;
}
// 反向查找
DNode<T> rnode = mHead.prev;
int rindex = mCount - index -1;
for (int j=0; j<rindex; j++)
rnode = rnode.prev;
return rnode;
}
// 獲取第index位置的節點的值
public T get(int index) {
return getNode(index).value;
}
// 獲取第1個節點的值
public T getFirst() {
return getNode(0).value;
}
// 獲取最后一個節點的值
public T getLast() {
return getNode(mCount-1).value;
}
// 將節點插入到第index位置之前
public void insert(int index, T t) {
if (index==0) {
DNode<T> node = new DNode<T>(t, mHead, mHead.next);
mHead.next.prev = node;
mHead.next = node;
mCount++;
return ;
}
DNode<T> inode = getNode(index);
DNode<T> tnode = new DNode<T>(t, inode.prev, inode);
inode.prev.next = tnode;
inode.next = tnode;
mCount++;
return ;
}
// 將節點插入第一個節點處。
public void insertFirst(T t) {
insert(0, t);
}
// 將節點追加到鏈表的末尾
public void appendLast(T t) {
DNode<T> node = new DNode<T>(t, mHead.prev, mHead);
mHead.prev.next = node;
mHead.prev = node;
mCount++;
}
// 刪除index位置的節點
public void del(int index) {
DNode<T> inode = getNode(index);
inode.prev.next = inode.next;
inode.next.prev = inode.prev;
inode = null;
mCount--;
}
// 刪除第一個節點
public void deleteFirst() {
del(0);
}
// 刪除最后一個節點
public void deleteLast() {
del(mCount-1);
}
}
2、循環單鏈表、循環雙鏈表(操作和單鏈、雙鏈是一樣的,不贅述了)
四、總結
- 本文主要是對於鏈表這種數據結構的介紹和認知,明白鏈表的優劣勢。
- 重點是要學會對於單鏈的操作,體會它的一些獨到之處,至於其它衍生鏈表,舉一反三而已!!!
個人博客地址:
cnblogs:https://www.cnblogs.com/baixianlong
csdn:https://blog.csdn.net/tiantuo6513
segmentfault:https://segmentfault.com/u/baixianlong
github:https://github.com/xianlongbai