前期提要:
在了解了顺序表和线性表的特性和应用之后,我们发现在插入元素的时候线性表所做的操作非常麻烦,插入中间元素时要把后面的所有的元素都往后挪,而且顺序表在插入元素的时候还要考虑表是否需要扩容。
链表就完美规避了这些问题,只需要断开元素之间的联系,再将插入元素与原链表元素重新建立联系即可,不需要对表进行扩容。
概念以及结构:
概念:
链表是一种物理结构上非连续的存储结构(存储内容在内存中并不一定连续——存储地址)
数据元素的逻辑顺序是通过链表中的引用链接次序实现的。
结构分类:
链表中有:单向、双向、带头节点、不带头结点、循环、不循环,这总共六种特点,由特点进行排列组合形成八种链表,其中linkedlist底层的链表就是 双向 不带头 非循环 类型链表
着重了解不带头、非循环的链表,是最常用到的链表类型 。
节点:
链表的节点由两部分组成:存放元素的数值域,存放下一个节点引用的next域
链表的链接方式:
不带头的链表:
带头的链表:
二者的区别:头结点的引用不一致 ,带头链表的头结点作为引用是固定的,不带头链表的头结点一旦换了结点,那头结点的引用也就跟着发生了改变(头结点的地址)
单向与双向:
单向链表:
单向链表的结点只能往下走,无法通过引用访问上一个节点
双向链表:
区别于单向链表,双向链表的结点引用既可以往后走,也可以往前走
通过引用当前节点的前驱prev,来访问当前节点的上一个节点
循环与不循环:
代码实现:
链表是引用对象,用一个内部类来实现节点类,节点类中包含了节点的各种成员变量(如果另外新建一个.java文件将创建结点类类,如果不用内部类的话则需要进行继承)
static class ListNode{
public int val; //节点值
public ListNode next; //节点的next域,后继结点
public ListNode prev; //节点的prev域,前驱结点
//对节点初始化
public ListNode(int val)
{
this.val = val;
}
}
链表的打印:
public void display() {
ListNode cur = head; //cur作为引用指向,当cur为空时说明已经走到链表尽头
while (cur!=null)
{
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
增:
头插法:
头插法就是把节点一个个串一起,先插入的头结点会变成后插入节点的下一个节点,也就是头结点因为节点的插入在不断地变化。
插入节点的实现方法:
不断变换头结点,让先插入的节点变成后插入节点的后继结点即可
public void addfirst(int data) {
ListNode node = new ListNode(data);
node.next = this.head;
this.head = node;
}
创建节点,然后创建表格进行插入操作
public static void main(String[] args) {
ListNode node1 = new ListNode(12);
ListNode node2 = new ListNode(23);
ListNode node3 = new ListNode(34);
ListNode node4 = new ListNode(56);
MySinglelist list = new MySinglelist();
list.addfirst(node1.val);
list.addfirst(node2.val);
list.addfirst(node3.val);
list.addfirst(node4.val);
list.display();
}
通过结果可以发现先插入的节点被挪到了后面
尾插法:
尾插法就是将第一个插入的元素作为头结点,在头结点不变的情况下不断地往后面插入节点。
在插入一个元素作为头结点的同时,链表的尾节点和头结点在同一起跑线上,后续有元素插入都是插入到尾节点后面,所以每次插入元素的操作都是修改将插入的节点修改为尾节点的后继结点
public void addlast(int data) {
ListNode node = new ListNode(data);
if(head == null) //先判断链表是否为空,如果为空那就将插入的节点作为头结点
{
head = node;
last = node;
}
else {
last.next = node; //往尾节点后插入元素
last = last.next;
}
}
创建节点,然后建表进行插入操作
public static void main(String[] args) {
ListNode node1 = new ListNode(12);
ListNode node2 = new ListNode(23);
ListNode node3 = new ListNode(34);
ListNode node4 = new ListNode(56);
ListNode node5 = new ListNode(67);
MySinglelist list = new MySinglelist();
list.addlast(node1.val);
list.addlast(node2.val);
list.addlast(node3.val);
list.addlast(node4.val);
list.addlast(node5.val);
list.display();
}
结果;
中间节点插入法:
在构思实现方法时,我们要有以下考虑:
1、实现方法中的返回参数类型、形参都有哪些?
2、既然是中间节点,说明不能插入到头结点前面和尾节点后面,插入的位置(也就是下标)要合理
3、插入的时候链表为空要怎么办?
4、插入的位置位于链表的哪个地方?怎么样插入才更加的快速?
解决问题1、2、3:
因为实现方法只是对链表进行操作,并不需要有返回值,所以返回类型为void,形参则要有插入的结点值和插入的位置index
public void addmid(int data,int index)
很简单,既然插入的结点不能作为头结点和尾节点,那么只要是下标 index≤0 或 index ≥ length 就是属于下标越界。
若链表为空,那么插入的结点就是头结点,也是尾节点
ListNode node = new ListNode(data);
int mid = size()/2;
int length = size();
if( index >= length || index <= 0) //下标判定
{
System.out.println("下标越界!");
return;
}
if(size()==0) //若链表为空
{
head = node;
last = head;
}
解决问题4:
我们插入中间节点node,只需要将(index-1)位置的后继结点改成node,然后将node的后继结点指向index位置的节点,这样就建立了节点之间的联系.
而在效率问题上,我们可以找到链表的中间位置mid,然后判断插入节点node在链表的前半段还是后半段。
index在链表前半段:
建立节点联系需要知道中间节点node的前驱结点和后继结点,而这就要遍历链表。
这里我们用需要两个指向分别记录node的前驱和后继结点,记录前驱节点的指向fast遍历速度要慢于记录后继结点slow的指向,由于是在链表中间插入,所以fast的起始位置是在第二个节点,而slow则是从头结点开始,这样fast指向往下走始终比slow快一步。 当fast走到index位置,便不再往下走,这时候将插入节点node改为slow的后继结点,node的后继结点就是fast指向的结点,这样就能建立插入节点与前驱和后继结点的联系
代码块:
if( index >= mid ) //若下标在链表的前半段
{
ListNode fast = head.next;
ListNode slow = head;
while( index-1 > 0 )
{
fast = fast.next;
slow = slow.next;
fast.prev = slow.next;
index--;
}
slow.next = node;
node.next = fast;
}
index位置刚好是尾节点在的位置:
这种情况分为两种:一种就是链表长度为2,头结点和尾节点不是同一个,另一种就是链表长度为1,头结点和尾节点是同一个
不过节点建立联系却很简单,这种情况下头结点就是slow指向,而尾节点就是fast指向,按照上述建立节点联系的思路即可
代码块:
else if ( mid == 0 || mid == 1) //链表长度是1
{
last.next = node;
last = last.next;
}
index位置在链表的后半段:
与index在链表前半段的思路差不多,反其道而行,fast指向在尾节点的前一个节点位置,每次往前遍历节点都是fast先往前走,然后slow指向再往前走
由于是要将节点node插入到index位置,所以在fast要走到index位置的前一个节点处,slow指向会走到index位置
代码块:
else //若下标在链表的后半段
{
ListNode fast = last.prev;
ListNode slow = last;
int num = length-index;
while( num >0)
{
fast = fast.prev;
slow.prev = fast.next;
num--;
}
fast.next = node;
node.next = slow.prev;
}
删:
删除节点就是要断开节点间的联系,删除节点分别需要三个引用:一个负责记录删除index下标节点的前驱节点slow,一个负责记录index下标节点的后继结点fast,一个负责判断是否到达index位置cur
实现删除指定下标节点,有几点需要考虑:
1、指定下标的合法性
2、头结点和尾节点是比较特殊的删除(也是最简单的删除)
3、如何断开节点间的联系
解决问题1:
由于下标是从0开始,那index的合法区间就是[ 0,length-1),在这个区间之外的下标都按越界处理
解决问题2和3:
如果删除的是头结点,只需要将头结点的后继结点改为新的头结点即可。
如果删除的是尾节点,那么就得分为两种情况:链表长度为2和长度大于2。如果链表长度等于2,那么只需要将头结点的后继结点改为null,然后把尾节点的引用改为头结点即可。如果大于2,那就得考虑三个引用的初始位置:跑的最慢的引用slow从起点开始,然后记录index位置的引用cur从头结点的下一个节点开始,跑的最快的引用fast在cur前面一步。
用引用cur所在的结点对应的下标len与index位置作差,计算三个引用要遍历的节点个数,当index-len=0,说明cur已经遍历到了要删除的结点
这时候将slow引用的后继结点改为fast即可完成删除操作。
完整代码块:
public void remove(int index) {
int length = size();
if(index >= length || index < 0 )
{
System.out.println("下标越界!");
}
else {
if(index==0) //删除的是头结点
{
ListNode cur = head.next;
head = head.next;
}
else{
if(length == 2) //删除的是尾节点且链表长度为2
{
head.next = null;
last = head;
}
else {
ListNode cur = head.next;
ListNode slow = head;
ListNode fast = cur.next;
int len =1;
while (index-len!= 0) {
fast = fast.next;
slow = slow.next;
cur = cur.next;
len++;
}
slow.next = fast;
}
}
}
}
改:
在明白了插入节点的思路之后,修改指定下标节点元素就是在用三个引用遍历到index位置所在的结点,然后将slow引用的后继结点改为要更改的结点node,再将node的后继结点改为fast即可。(当然,要记得进行下标合法性判断)
看到这直接上代码块,思路与删的无异:
public void replace(int data, int index) {
ListNode node = new ListNode(data);
int length = size();
if(index>=length || index<0){
System.out.println("下标越界!");
}
else {
ListNode cur = head.next;
ListNode slow = head;
ListNode fast = cur.next;
int len =1;
while (index-len!= 0) {
fast = fast.next;
slow = slow.next;
cur = cur.next;
len++;
}
slow.next = node;
node.next = fast;
}
}
查:
查找有两种:用下标找节点,找链表中是否有该节点。这两种都至少简单的遍历链表,然后将结果返回。直接上代码:
@Override
public boolean search(int key) {
ListNode cur = head;
while(cur!=null)
{
if(cur.val==key)
{
return true;
}
cur = cur.next;
}
return false;
}
//查找index位置所在的结点
@Override
public void find(int index) {
int length = size();
if(index>=length || index<0)
{
System.out.println("下标越界!");
}
else {
ListNode cur = head;
while(index !=0)
{
cur = cur.next;
index--;
}
System.out.println("index位置节点的值是: "+cur.val);
}
}
最后把完整的代码奉上:
public class MySinglelist implements MyList {
public ListNode head;
public ListNode last;
//内部类创建链表
static class ListNode{
public int val; //节点值
public ListNode next; //节点的next域
public ListNode prev; //节点的next域
//对节点初始化
public ListNode(int val)
{
this.val = val;
}
}
//打印链表
@Override
public void display() {
ListNode cur = head;
while (cur!=null)
{
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
//头插法
@Override
public void addfirst(int data) {
ListNode node = new ListNode(data);
node.next = this.head;
this.head = node;
}
@Override
public void addmid(int data,int index) {
ListNode node = new ListNode(data);
int mid = size()/2;
int length = size();
if( index >= length || index <= 0) //下标判定
{
System.out.println("下标越界!");
return;
}
if(size()==0) //若链表为空
{
head = node;
last = head;
}
else {
if( index >= mid ) //若下标在链表的前半段
{
ListNode fast = head.next;
ListNode slow = head;
while( index-1 > 0 )
{
fast = fast.next;
slow = slow.next;
fast.prev = slow.next;
index--;
}
slow.next = node;
node.next = fast;
}
else if ( index == length || mid == 0) //若下标刚好与目前的链表长度相等或者下标是1
{
last.next = node;
last = last.next;
}
else //若下标在链表的后半段
{
ListNode fast = last.prev;
ListNode slow = last;
int num = length-index;
while( num >0)
{
fast = fast.prev;
slow.prev = fast.next;
num--;
}
fast.next = node;
node.next = slow.prev;
}
}
}
//尾插法
@Override
public void addlast(int data) {
ListNode node = new ListNode(data);
if(head == null) //先判断链表是否为空,如果为空那就将插入的节点作为头结点
{
head = node;
last = node;
}
else {
last.next = node;
last = last.next;
}
}
//返回链表长度
@Override
public int size() {
ListNode cur = head;
int size = 0;
while(cur!=null)
{
cur = cur.next;
size++;
}
return size;
}
//删除指定节点元素
@Override
public void remove(int index) {
int length = size();
if(index >= length || index < 0 )
{
System.out.println("下标越界!");
}
else {
if(index==0) //删除的是头结点
{
ListNode cur = head.next;
head = head.next;
}
else{
if(length == 2) //删除的是尾节点且链表长度为2
{
head.next = null;
last = head;
}
else {
ListNode cur = head.next;
ListNode slow = head;
ListNode fast = cur.next;
int len =1;
while (index-len!= 0) {
fast = fast.next;
slow = slow.next;
cur = cur.next;
len++;
}
slow.next = fast;
}
}
}
}
//改变指定节点元素
@Override
public void replace(int data, int index) {
ListNode node = new ListNode(data);
int length = size();
if(index>=length || index<0){
System.out.println("下标越界!");
}
else {
ListNode cur = head.next;
ListNode slow = head;
ListNode fast = cur.next;
int len =1;
while (index-len!= 0) {
fast = fast.next;
slow = slow.next;
cur = cur.next;
len++;
}
slow.next = node;
node.next = fast;
}
}
//查找链表是否存在对应的节点元素
@Override
public boolean search(int key) {
ListNode cur = head;
while(cur!=null)
{
if(cur.val==key)
{
return true;
}
cur = cur.next;
}
return false;
}
//查找index位置所在的结点
@Override
public void find(int index) {
int length = size();
if(index>=length || index<0)
{
System.out.println("下标越界!");
}
else {
ListNode cur = head;
while(index !=0)
{
cur = cur.next;
index--;
}
System.out.println("index位置节点的值是: "+cur.val);
}
}
public static void main(String[] args) {
ListNode node1 = new ListNode(12); // 0
ListNode node2 = new ListNode(34); // 1
ListNode node3 = new ListNode(45); // 2
ListNode node4 = new ListNode(56); // 3
ListNode node5 = new ListNode(67); // 4
ListNode node6 = new ListNode(23);
MySinglelist list = new MySinglelist();
list.addlast(node1.val);
list.addlast(node2.val);
list.addlast(node3.val);
list.addlast(node4.val);
list.addlast(node5.val);
/* System.out.println(list.search(67));*/ // 查
/*list.find(2);*/
/* list.remove(3); //删
list.display();*/
/*list.replace(node6.val,3); //改
list.display();*/
/* list.addmid(node6.val,2); //中间插入节点
list.display();*/
}
}
因为我本人使用一个接口来实现链表的所有功能,所以主类里的所有方法都是经过重写的方法,在此附上接口的代码(主类中用alt+insert快捷键一键添加重写方法,详情跳转接口博客:http://t.csdnimg.cn/pI0uu)
public interface MyList {
void addfirst(int data);
void addlast(int data);
void addmid(int data,int index);
void display();
void remove(int index);
void replace(int data,int index);
boolean search(int key);
void find(int index);
int size();
}
看到最后给个三连吧!!!