链表
链表通过指针将一组零散的内存块串联在一起。其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。
链表也是线性表;
特点
- 不需要连续的内存空间。
- 有指针引用
- 三种最常见的链表结构:单链表、双向链表和循环链表
单链表
从单链表图中,可以发现,有两个结点是比较特殊的,它们分别是第一个结点和最后一个结点。我们一般把第一个结点叫作头结点,把最后一个结点叫作尾结点。
其中,头结点用来记录链表的基地址。通过他可以遍历得到整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址NULL,表示这是链表上最后一个结点。
while(p.next != null){} 通常用来当作链表是否为最后一个结点。
LinkedList与ArrayList
从链表的查找、插入和删除进行比较。
数据结构分析、ArrayList底层是数组、而LinkedList底层是Node结点,用指针相连。
- 查找: 由于ArrayList底层数组的随机访问特性、定位元素的速度远比LinkedList快得多,因为LinkedList每次都需要从头结点遍历找到合适的结点。
- 插入、删除:ArrayList底层数组插入、删除元素会经常涉及到移位操作,而LinkedList只需要改变指针指向就可以完成插入删除操作。因为,插入、删除上LinkedList比ArrayList更高效;
数组VS链表
重要区别:
- 数组简单易用,在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高。
- 链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法有效预读。
- 数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out ofmemory)”。如果声明的数组过小,则可能出现不够用的情况。
- 动态扩容:数组需再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容。
循环链表
循环链表是一种特殊的单链表。实际上,循环链表也很简单。它跟单链表唯一的区别就在尾结点。我们知道,单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。从我画的循环链表图中,你应该可以看出来,它像一个环一样首尾相连,所以叫作“循环”链表。
双向链表
- 双向链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针next指向后面的结点,还有一个前驱指针prev指向前面的结点。
- 双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。
- 虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。
单项链表实现
package algorithm.list;
public class MyLinkedList {
private ListNode head;
private int size = 0; //
public void insertHead(int data){ //插入链表的头部 data就是插入的数据
ListNode newNode = new ListNode(data);
//如果原来就有数据呢?
newNode.next = head; //栈内存的引用
head = newNode;
//插入O(1)
}
public void insertNth(int data,int position){ //插入链表的中间 假设定义在第N个插入 O(n)
if(position == 0) { //这个表示插入在头部了
insertHead(data);
}else{
ListNode cur = head;
for(int i = 1; i < position ; i++){
cur = cur.next; //一直往后遍历 p=p->next; ->是c++里面的往后找指针
}
ListNode newNode = new ListNode(data);
//
newNode.next = cur.next; //新加的点指向后面 保证不断链
cur.next = newNode; //把当前的点指向新加的点
}
}
/*int a = 1;
int b = a;
int a = 2;*/
public void deleteHead(){//O(1)
head = head.next;
}
public void deleteNth(int position){//O(n)
if(position == 0) {
deleteHead();
}else{
ListNode cur = head;
for(int i = 1; i < position ; i ++){
cur = cur.next;
}
cur.next = cur.next.next; //cur.next 表示的是删除的点,后一个next就是我们要指向的
}
}
public void find(int data){//O(n)
ListNode cur = head;
while(cur != null){
if(cur.value == data) break;
cur = cur.next;
}
}
public void print(){
ListNode cur = head;
while(cur != null){
System.out.print(cur.value + " ");
cur = cur.next;
}
System.out.println();
}
public static void main(String[] args) {
MyLinkedList myList = new MyLinkedList();
myList.insertHead(5);
myList.insertHead(7);
myList.insertHead(10);
myList.print(); // 10 -> 7 -> 5
myList.deleteNth(0);
myList.print(); // 7 -> 5
myList.deleteHead();
myList.print(); // 5
myList.insertNth(11, 1);
myList.print(); // 5 -> 11
myList.deleteNth(1);
myList.print(); // 5
}
}
class ListNode{
int value; //值
ListNode next; //下一个的指针
ListNode(int value){
this.value = value;
this.next = null;
}
}
双向链表实现
package algorithm.list;
public class DoubleLinkList { // 双向链表
private DNode head; //头
private DNode tail; // 尾
DoubleLinkList(){
head = null;
tail = null;
}
public void inserHead(int data){
DNode newNode = new DNode(data);
if(head == null){
tail = newNode;
}else{
head.pre = newNode;
newNode.next = head;
}
head = newNode;
}
public void deleteHead(){
if(head == null) return ; //没有数据
if(head.next == null){ //就一个点
tail = null;
}else{
head.next.pre = null;
}
head = head.next;
}
public void deleteKey(int data){
DNode current = head;
while (current.value != data) {
if (current.next == null) {
System.out.println("没找到节点");
return ;
}
current = current.next;
}
if (current == head) {// 指向下个就表示删除第一个
deleteHead();
} else {
current.pre.next = current.next;
if(current == tail){ //删除的是尾部
tail = current.pre;
current.pre = null;
}else{
current.next.pre = current.pre;
}
}
}
}
class DNode{
int value; //值
DNode next; //下一个的指针
DNode pre; //指向的是前一个指针
DNode(int value){
this.value = value;
this.next = null;
this.pre = null;
}
}
重要是链表的思想,而不是代码。