链表
单向链表
单向链表概念
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
这是一种物理结构,不是树那样的逻辑结构。链表和顺序表两种物理结构,说明的是数据元素在内存中是如何存储的。顺序表是连续顺序存储,例如数组。而链表是非连续非顺序的。
基本API的java代码实现
代码如下,实现基本的增查删改的功能。
public class SinglyLinkedList<T> {
private class Node{
private T value;
private Node next;
public Node(T value){
this.value=value;
this.next=null;
}//constructor
}
private Node head;
private Node tail;
public SinglyLinkedList(){
this.head=null;
this.tail=null;
}
//empty list?
public boolean Empty(){
return this.head==null;
};
//add to front
//两种情况:链表为空和正常
public void PushFront(T key){
Node node=new Node(key);
node.next=this.head;
this.head=node;
if(this.tail==null) this.tail=this.head;//原本无元素的话,需要将尾节点设置为刚插入的新节点
}
//return front item
public T TopFront(){
if(Empty()) throw new IndexOutOfBoundsException("linked list is empty");
return head.value;
}
//remove front item
//两种情况:只有一个元素和正常
public void PopFront(){
if(Empty()) return;
this.head=this.head.next;
if(this.head==null) this.tail=null;//只有一个元素,pop后变为空链表
}
//add to back
public void PushBack(T key){
Node node=new Node(key);
if(this.tail==null){
this.head=this.tail=node;
}
else{
this.tail.next=node;
this.tail=node;
}
}
//return back item
public T TopBack(){
if(Empty()) throw new IndexOutOfBoundsException("linked list is empty");
return tail.value;
}
//remove back item
//三种情况:只有一个元素和两个元素和正常
public void PopBack(){
if(Empty()) return;
if(this.head==this.tail) this.head=this.tail=null;//只有一个元素,无法遍历到倒数第二个
else {
Node node = this.head;
while (node.next.next != null) node = node.next;//遍历到倒数第二个元素
this.tail = node;
node.next=null;
}
}
//is key in list?
public boolean Find(T key){
Node node=this.head;
while(node!=null){
if(node.value==key) return true;
node=node.next;
}
return false;
}
//remove key from list
public void Erase(T key){
Node node=this.head;
while(node!=null){
if(node.value==key){
if(this.head==this.tail) this.head=this.tail=null;//只有一个元素
else if(node==this.head) this.head=this.head.next;//删除的是头元素
else{
Node temp=this.head;
while(temp.next!=node) temp=temp.next;//遍历到node的前一个节点
temp.next=node.next;
if(node==this.tail) this.tail=temp;//如果删除的是尾节点,要改变尾节点
}
}
node=node.next;
}
}
//adds keys before node
public void AddBefore(Node node,T key){
Node New=new Node(key);
if(node==this.head){
New.next=this.head;
this.head=New;
}//插在头节点前不用遍历
else{
Node temp=this.head;
while(temp!=null){
if(temp.next==node){
temp.next=New;
New.next=node;
}
temp=temp.next;
}
}
}
//adds key after node
public void AddAfter(Node node,T key){
Node New=new Node(key);
New.next=node.next;
node.next=New;
if(node==this.tail) this.tail=New;//如果插在尾节点后要变更尾节点
}
//print the value of the whole linked list
public void Print(){
Node node=this.head;
while(node!=null) {
System.out.print(node.value+" ");
node=node.next;
}
System.out.println();
}
public Node getHead(){
return head;
}
public Node getNode(){
return head.next.next.next;
}
public static void main(String[] args){
SinglyLinkedList<Integer> list= new SinglyLinkedList<>();
for(int i=5;i>0;i--) list.PushFront(i);//test PushFront
for(int i=6;i<11;i++) list.PushBack(i);//test PushBackT
list.Print();
System.out.println(list.TopFront()+" "+list.TopBack());//test TopFront and TopBack
list.PopFront();
list.PopBack();
list.Print();
list.AddBefore(list.getHead(),11);
list.AddBefore(list.getNode(),12);
list.Print();
list.AddAfter(list.getHead(),13);
list.AddAfter(list.getNode(),14);
list.Print();
System.out.println(list.Find(22)+" "+list.Find(9));
list.Erase(11);
list.Print();
}
}
各个API的时间复杂度O(n)
接下来分析时间复杂度
Singly-Linked List | no tail | with tail |
---|---|---|
PushFront(key) | O(1) | |
TopFront() | O(1) | |
PopFront() | O(1) | |
PushBack(key) | O(n) | O(1) |
TopBack() | O(n) | O(1) |
PopBack() | O(n) | |
Find(key) | O(n) | |
Erase(key) | O(n) | |
Empty() | O(1) | |
AddBefore(Node,Key) | O(n) | |
AddAfter(Node,Key) | O(1) |
很显然,每当我们的操作是从后往前进行时,复杂度就会很高。因为我们需要从头开始遍历.例如PopBack,我们需要从头开始遍历。像二分搜索,更是难以实现。如果有一个和next相对的previous指针,事情会简单很多。这就是双向链表的好处。
双向链表
双向链表概念
功能和单向链表一样,就是每次操作时要多对prev节点操作。每次插入和删除操作时,要处理四个节点的引用,而不是两个。
单向链表插入时,我们只需考虑改变插入节点的前一个节点的next(如果不是插入在头节点前),和插入节点的next。而双向链表插入时我们需要考虑插入节点的前一个节点的next,插入节点的prev和next,插入节点的后一个节点的prev((如果不是插入在尾节点后)。所以需要特别仔细。
基本API的代码实现
public class DoublyLinkedList<T> {
private class Node{
private T value;
private Node next;
private Node prev;
public Node(T value){
this.value=value;
this.next=null;
this.prev=null;
}
}
private Node head;
private Node tail;
public DoublyLinkedList(){
this.head=null;
this.tail=null;
}
//empty list?
public boolean Empty(){
return this.head==null;
}
//add to front
//两种情况:链表为空和正常
public void PushFront(T key){
Node node=new Node(key);//node.prev=null;
node.next=this.head;
this.head=node;
if(this.tail==null) this.tail=this.head;//原本无元素的话,需要将尾节点设置为刚插入的新节点
else this.head.next.prev=node;//原本有元素的话设置原head的prev节点
}
//return front item
public T TopFront(){
if(Empty()) throw new IndexOutOfBoundsException("linked list is empty");
return head.value;
}
//remove front item
//两种情况:只有一个元素和正常
public void PopFront(){
if(Empty()) return;
if(this.head!=this.tail) this.head.next.prev=null;//两个元素及以上要设置prev节点
this.head=this.head.next;
if(this.head==null) this.tail=null;//只有一个元素,pop后变为空链表
}
//add to back
public void PushBack(T key){
Node node=new Node(key);
if(this.tail==null){
this.head=this.tail=node;
}
else{
this.tail.next=node;
node.prev=this.tail;
this.tail=node;
}
}
//return back item
public T TopBack(){
if(Empty()) throw new IndexOutOfBoundsException("linked list is empty");
return tail.value;
}
//remove back item
//拥有prev节点后无须从头遍历
public void PopBack(){
if(Empty()) return;
if(this.head==this.tail) this.head=this.tail=null;
else{
this.tail=this.tail.prev;
this.tail.next=null;
}
}
//is key in list?
public boolean Find(T key){
Node node=this.head;
while(node!=null){
if(node.value==key) return true;
node=node.next;
}
return false;
}
//remove key from list
public void Erase(T key){
Node node=this.head;
while(node!=null){
if(node.value==key){
if(this.head==this.tail) this.head=this.tail=null;//只有一个元素
else if(node==this.head) {
this.head=this.head.next;//删除的是头元素
this.head.prev=null;
}
else{
node.prev.next=node.next;
if(node==this.tail) this.tail=node.prev;//如果删除的是尾节点,要改变尾节点
else node.next.prev=node.prev; //非尾节点要改变被删节点的下一个节点的prev
}
}
node=node.next;
}
}
//adds keys before node
//拥有prev节点后不用遍历
public void AddBefore(Node node, T key){
Node New=new Node(key);
if(node==this.head){
New.next=this.head;
this.head.prev=New;
this.head=New;
}
else{
node.prev.next=New;//改变被插入节点前一个节点的next节点
New.prev=node.prev;//设置插入节点的prev节点
New.next=node;
node.prev=New;
}
}
//adds key after node
public void AddAfter(Node node, T key){
Node New=new Node(key);
New.next=node.next;
New.prev=node;
node.next=New;
if(node==this.tail) this.tail=New;//如果插在尾节点后要变更尾节点
else node.next.prev=New;//插入的不在尾节点后,就需要要改变插入位置后一个节点的prev节点
}
//print the value of the whole linked list
public void Print(){
Node node=this.head;
while(node!=null) {
System.out.print(node.value+" ");
node=node.next;
}
System.out.println();
}
public Node getHead(){
return head;
}
public Node getNode(){
return head.next.next.next;
}
public static void main(String[] args){
DoublyLinkedList<Integer> list= new DoublyLinkedList<>();
for(int i=5;i>0;i--) list.PushFront(i);//test PushFront
for(int i=6;i<11;i++) list.PushBack(i);//test PushBackT
list.Print();
System.out.println(list.TopFront()+" "+list.TopBack());//test TopFront and TopBack
list.PopFront();
list.PopBack();
list.Print();
list.AddBefore(list.getHead(),11);
list.AddBefore(list.getNode(),12);
list.Print();
list.AddAfter(list.getHead(),13);
list.AddAfter(list.getNode(),14);
list.Print();
System.out.println(list.Find(22)+" "+list.Find(9));
list.Erase(11);
list.Erase(9);
list.Erase(13);
list.Print();
}
}
时间复杂度O(n)
Doubly-Linked List | no tail | with tail |
---|---|---|
PushFront(key) | O(1) | |
TopFront() | O(1) | |
PopFront() | O(1) | |
PushBack(key) | O(n) | O(1) |
TopBack() | O(n) | O(1) |
PopBack() | ||
Find(key) | O(n) | |
Erase(key) | O(n) | |
Empty() | O(1) | |
AddBefore(Node,Key) | ||
AddAfter(Node,Key) | O(1) |
很明显,有了prev节点后,我们的PopBack和AddBefore时间复杂度都降低了,因为省去了遍历的过程。当然 ,由于多了两个链节点的引用,链节点占用的空间也会变大。所以使用双向链表就是用空间换时间。