1、线性表之链表
1、链表-概述:
1-1、n个节点离散分配,彼此通过指针相连,每个元素包含两个节点,一个是存储元素的数据域 (内存空间),另一个是指向下一个结点地址的指针域,首节点没有前驱节点,尾节点没有后续节点。
1-2、确定一个链表我们只需要头指针,通过头指针就可以把整个链表都能推出来。
1-3、链表一个链表节点的数据结构:value值+next指针。
1-4、是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可。
1-5、对于链表的新增,删除等操作(在找到指定操作位置后),时间复杂度为O(1), 而查找操作需要遍历链表逐一进行比对,复杂度为O(n)。
2、链表的-优点:
2-1、链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素;
2-2、添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快;
3、链表的-缺点:
3-1、因为含有大量的指针域,占用空间较大;
3-2、查找元素需要遍历链表来查找,非常耗时。
4、适用场景:
4-1、数据量较小,需要频繁增加,删除操作的场景
5、链表-单链表-举例Demo
public class Node {
public Node next; //下一个节点
private Object data; //链表存储的数据
public Node(Object data) {
this.data = data;
}
}
//循环遍历-单链表
public static void getDataByLoop(Node node){
while (node != null) {
System.out.println(node.getData()+"");
node = node.getNext();
}
}
//递归遍历-单链表
public static void getDataByRecursion(Node node) {
if (node == null) {
return;
}
System.out.println(node.getData()+" "); //15 1 9
getDataByRecursion(node.getNext());
}
//链表:链表是一种物理存储单元上非连续、非顺序的存储结构
//特点:插入、删除时间复杂度O(1) 查找遍历时间复杂度0(N) 插入快、查找慢
public static void main(String[] args) {
Node node=new Node(15);
node.next=new Node(1);
node.next.next=new Node(9);
System.out.println("使用while循环遍历链表");
getDataByLoop(node);
System.out.println();
System.out.println("使用递归遍历链表");
getDataByRecursion(node);
System.out.println();
}
1.1、链表-分类
1、链表分类:根据指针的指向,链表能形成不同的结构
1-1、单向链表: 一个节点指向下一个节点。
1-2、双向链表: 一个节点有两个指针域。
1-3、环形链表: 能通过任何一个节点找到其他所有的节点,将两种(双向/单向)链表的最后一个结点指向第一个结点从而实现循环。
1-3-1、应用:比如:单向环形链表->约瑟夫问题?
1.2、链表之单链表分析
1、链表之单链表分析:
1-1、单链表结构:head头节点->首节点->xxxx->尾节点->next[null]
1-1-1、头节点:不存放具体的数据,只用来表时单链表头next。
1-2、
2、单链表遍历-伪代码:
/**
* 遍历链表
* @param head 头节点
*/
public static void traverse(Node head) {
//临时节点,从首节点开始
Node temp = head.next;
// 为null则说明到了链表的尾部
while (temp != null) {
System.out.println("链表数据:" + temp.data);
//继续下一个,
temp = temp.next;
}
}
3、单链表-添加(创建)-伪代码:
3-1、先创建一个head头结点,
3-2、当不考虑编号顺序时,每添加一个节点,直接加入到链表的最后(遍历链表)
3-3、考虑编号顺序时,遍历链表:
3-3-1、修改:
1、先创建一个head头结点
2、根据编号找到需要修改信息的节点(遍历链表)
public static void update(int value, Node Head){
//初始化要加入的节点
Node newNode = new Node(value);
//临时节点,因为head节点不能动,所以需要一个辅助遍历节点 temp
Node temp = head;
// 找到尾节点,temp.next为null,则说明找到尾节点了。
while (temp.next != null) {
// 找到节点信息
if(temp.no = newNode.no){
// 将找到节点信息更新为新的信息
temp.xxx = newNode.xxx;
}
// 没有找到最后,则将temp后移
temp = temp.next;
}
// temp.next=null,说明到了尾节点了,将最后这个节点的next 指向新的节点
// 到链表的尾部了,还没匹配上需要修改节点的信息,不做处理
}
3-3-2、删除:
1、先创建一个head头结点
2、先找到需要删除的这个节点的前一个节点(temp.next.no = delNode.no)
3、将待删除节点的前一个节点下一个指向(就是指向待删除的节点),修改为下下一个节点(将指向待删除节点的前一个节点指针指向待删除节点的下一个节点)
temp.next = temp.next.next
4、被删除的节点,将不会有其他引用指向,会被垃圾回收机制回收掉。
1.2.1、链表之单链表分析代码示例
1、当不考虑编号顺序时,思路:
1.找到当前链表的最后节点
2.将最后这个节点的next,指向新的节点
public static void addData(int value, Node head) {
//初始化要加入的节点
Node newNode = new Node(value);
//临时节点,因为head节点不能动,所以需要一个辅助遍历节点 temp
Node temp = head;
// 找到尾节点,temp.next为null,则说明找到尾节点了。
while (temp.next != null) {
// 没有找到最后,则将temp后移
temp = temp.next;
}
// temp.next=null,说明到了尾节点了,将最后这个节点的next 指向新的节点
temp.next = newNode;
}
//显示链表(遍历)
public void list(){
//判断链表是否为空
if(head.next==null){
System.out.println("链表为空");
return;
}
//因为头结点,不能动,因此我们需要一个辅助变量来遍历
Node temp = head.next;
//判断是否到链表最后,为null则表示到链表最后
while(temp != null){
//输出节点信息
System.out.println(temp.toString());
//将next后移 一定注意
temp = temp.next;
}
}
2、考虑编号顺序时,思路:
1、找到新添加的节点的位置,通过辅助遍历(指针),遍历链表
// 因为单链表,因为我们找到temp是位于添加位置的前一个节点
temp = temp.next; // 后移,遍历当前链表
2、将新节点的next指向temp的next,
newNode.next = temp.next;
3、将temp的next指向新节点
temp.next = newNode;
// 考虑编号顺序时,添加新的节点
public void addByOrder(HeroNode heroNode){
//因为头结点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
//因为单链表,因为我们找到temp是位于添加位置的前一个节点,否则插入不了
HeroNode temp =head;
boolean flag = false;//flag 标识添加的编号是否为存在,默认为false
while(true){
if(temp.next==null){//说明temp已经在链表最后
break;
}
if(temp.next.no>heroNode.no){//位置找到,就在temp的后面插入
break;
}else if(temp.next.no==heroNode.no){//说明希望添加的heroNode编号已经存在 不能再添加(规定)
flag =true;//说明编号存在
break;
}
temp =temp.next;//后移,遍历当前链表
}
//判断flag的值
if(flag){//不能添加,说明编号存在
System.out.printf("准备插入的英雄的编号%d已经存在了,不能加入\n",heroNode.no);
}else {
//插入到链表中 temp后面
heroNode.next=temp.next;
temp.next=heroNode;
}
}
1.2.2、链表之单链表-面试题
1、求一个单链表的节点个数(头结点不保存数据):注意检查链表是否为空
public int getLength(Node head) {
if (head == null) {
return 0;
}
int length = 0;
// 头结点不保存数据,统计有效节点时:需要将头结点去掉
// Node current = head.next;
Node current = head;
while (current != null) {
length++;
// 将节点后移
current = current.next;
}
return length;
}
2、查找单链表中倒数第K个节点
思路:1、先定义temp变量接收head节点,index:表示倒数第k个节点的节点下标
2、链表遍历,得到链表的长度(size)
3、再次遍历链表,范围:size - index
3、单链表的反转
/**
head节点信息:
head = {HeroNode@631} "HeroNode{no=0, name='', nickname=''}"
head.next = {HeroNode@632} "HeroNode{no=1, name='宋江', nickname='及时雨'}"
head.next.next = {HeroNode@633} "HeroNode{no=2, name='卢俊义', nickname='玉麒麟'}"
第一次遍历:
cur = {HeroNode@632} "HeroNode{no=1, name='宋江', nickname='及时雨'}"
next = {HeroNode@633} "HeroNode{no=2, name='卢俊义', nickname='玉麒麟'}"
reverseHead = {HeroNode@654} "HeroNode{no=0, name='', nickname=''}"
head = {HeroNode@631} "HeroNode{no=0, name='', nickname=''}"
head.next = {HeroNode@632} "HeroNode{no=1, name='宋江', nickname='及时雨'}"
reverseHead.next = {HeroNode@632} "HeroNode{no=1, name='宋江', nickname='及时雨'}"
public void reverse(){
//如果当前链表为空,或者只有一个节点,无需反转,直接发返回
if(head.next==null||head.next.next==null){
return;
}
//定义一个辅助指针(变量),帮助我们遍历原来的链表
HeroNode cur = head.next;
HeroNode next = null;//指向当前节点的下一个节点
HeroNode reverseHead = new HeroNode(0,"","");
//遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reversehead的最前端
while(cur!=null)//cur=null说明遍历结束
{
next = cur.next;//先暂时保存当前节点的下一个节点,因为后面需要使用
cur.next = reverseHead.next;//将cur的下一个节点指向新的链表的最前端
reverseHead.next=cur;//将cur连接到新的链表上
cur = next;//让cur后移
}
//连接head.next指向reverseHead.next 实现单链表反转
head.next = reverseHead.next;
}
*/
3-1、头结点插入方式
/**
头结点插入法的实质是重新创建了一个新的链表,通过遍历待反转链表,将链表每一个节点插入到创建的链表中,然后的到的这个创建的链表就是反转后的链表。
*/
public static ListNode reverseListByInsert(ListNode listNode){
//定义一个带头节点的,保存反转之后的链表信息
ListNode resultList = new ListNode(-1); // 此时反转的链表只有一个头结点
//循环节点,初始值为待反转的链表
ListNode p = listNode;
// 遍历待反转的链表
while(p!= null){
//定义一个链式链表,用来保存p.next的值,用于下一次循环
// 第一次遍历时,p.next = {1},故ListNode tempList = {1}
ListNode tempList = p.next;
// 将p.next(当前指向的首节点)与反转后的链表的头结点之后的节点连接起来
// 第一次遍历的时候,此时反转的链表只有一个头结点,resultList.next = null,故:p.next = null
p.next = resultList.next;
// 将拼接起来的节点 拼接到反转后链表的头结点的后面
// 第一次遍历,p = ListNode{0},故:resultList = {-1},resultList.next = ListNode{0},resultList.next.next = null
resultList.next = p;
// 第一次遍历后,p = tempList = {1}
p = tempList;
}
// resultList.next 是去除掉头结点-1的值
return resultList.next;
}
3-2、就地反转方式
/**
在待反转链表基础上修改节点顺序得到反转链表。
难点在于理解循环中resultList.next指向性的变化,以及p和pNext两个变量的变化, p指向的链表首结点永远是1,只是节点1在resultList链表中位置在发生变化,而pNext是随着p移动的。
resultList = {HeroNode@634} "HeroNode{no=-1, name='', nickname=''}"
p = {HeroNode@631} "HeroNode{no=0, name='', nickname=''}"
pNext = {HeroNode@639} "HeroNode{no=1, name='宋江', nickname='及时雨'}"
pNext.next = {HeroNode@652} "HeroNode{no=2, name='卢俊义', nickname='玉麒麟'}"
p.next = {HeroNode@639} "HeroNode{no=1, name='宋江', nickname='及时雨'}"
resultList.next = {HeroNode@631} "HeroNode{no=0, name='', nickname=''}"
*/
public static ListNode reverseListByLocal(ListNode listNode){
// 定义一个带头节点的,保存反转之后的链表信息
// resultList: -1 -> null
ListNode resultList = new ListNode(-1);
// 将保存反转后的链表的 头结点指向待反转的链表。
// resultList : -1 -> 0 -> 1 -> 2 -> 3 ->4
resultList.next= listNode;
// 循环节点,初始值为待反转的链表
// p: 0 -> 1 -> 2 -> 3 ->4
ListNode p = listNode;
// 定义一个链式链表,用来保存p.next的值,用于下一次循环
// pNext: 1 ->2 ->3 -> 4
ListNode pNext = p.next;
while (pNext!=null){
// 第一次遍历,pNext.next = HeroNode{2}
// p: 0 -> 2 -> 3 ->4 -> null
p.next = pNext.next;
// 第一次遍历,resultList.next = HeroNode{0}
// pNext: 1 -> 0 -> 2 -> 3 ->4
// resultList: -1 ->0 -> 2 -> 3 ->4 -> null ??? 理解->1 去哪里了
pNext.next = resultList.next;
// 让resultList头结点指向pNext
// 第一次遍历,pNext = HeroNode{1}
// resultList:
resultList.next = pNext;
// 让pNext指向p的下一个节点
pNext=p.next;
}
return resultList.next;
}
1.3、链表之双向链表分析
1、双向链表可以向前或者向后 单链表需依靠辅助节点进行删除
3、双向链表结构图:prev+Element+next
4、操作:
4-1、遍历:与单链表一样,只是可以向前,也可以向后查找
4-2、添加(链表尾部):
先找到双向链表的尾部节点(设置为temp)
temp.next = newNode;
newNode.pre = temp;
4-3、删除:可自我删除
先找到双向链表的删除节点(设置为temp)
temp.pre.next = temp.next;
temp.next.pre = temp.per;