单向链表
如上图:
1.链表是以节点的方式来存储的;
2.每个节点包括了data域和next域,其中data域就是存放元素的,next域就是指向下一个节点的;
3.链表的每个节点都不一定是连续存储的;
4.链表根据结构可以分为单向链表和双向链表,根据需求可以选择带头结点的链表和不带头节点的链表(环形链表);
流程分析
如上图我们如果想要使用代码实现单向链表:
1.需要定义节点、链表的长度、节点的data域以及next域以及头节点和尾节点的标识;
2.对于新增方法,我们选择尾插法,也就是让尾节点的next域指向新增的节点;
3.对于删除方法,我们需要找到需要删除的节点,然后让它的上一个节点的next域指向它的next域;
4.对于查询方法,我们遍历链表,从头节点,挨个挨个next节点的去找,直到找到目标节点;
代码实现
public class StrineList<T> {
Node<T> first;
Node<T> last;
int size;
/**
*
* @Author:strine
* 定义的节点
* */
public static class Node<T>{
T data;
Node<T> next;
Node(Node<T> next,T data){
this.data=data;
this.next=next;
}
}
/**
*
* @Author:strine
* 新增方法
* */
public void put(T t){
//获取最后一个节点,并临时保存
Node<T> la =last;
//根据传入的参数生成新的节点
Node<T> newNode = new Node<>(null, t);
//将该节点使用尾插法插入链表尾部
last=newNode;
//判断临时保存的链表尾节点是否为null
if (la==null){
//第一次添加
first=newNode;
}else {
//如果不为null就把新节点添加到最开始的尾结点的后方
la.next=newNode;
}
size++;
}
/**
*
* @Author:strine
* 根据下标的删除方法
* */
public void remove(int index){
//删除操作其实就是将上一个节点的指针指向目标节点的下一个节点
//找到目标节点和上一个节点
Node<T> node = getNode(index);
Node<T> prev = getNode(index - 1);
if (node==last){
//若目标节点为尾节点,则让尾节点指向上一个节点
last=prev;
}else {
// 否则就让上一个节点的next域指向目标节点的next域
prev.next=node.next;
}
// 告诉GC去回收垃圾
node=null;
size--;
}
/**
*
* @Author:strine
* 获取节点
* */
public Node<T> getNode(int index){
//获取到头节点
Node<T> tar = first;
if (index<=size){
// 挨着下个节点下个节点的遍历,直到找到目标节点
for (int i = 0; i < index; i++) {
tar=tar.next;
}
return tar;
}
return null;
}
/**
*
* @Author:strine
* 获取目标值
* */
public T get(int index){
Node<T> node = getNode(index);
return node.data;
}
}
双向链表
如上图:
1.双向链表在单向链表的基础上需要额外维护每个节点的prev域;
2.因此我们在新增和删除的时候,不仅要关注next域的指向,还需要关注prev域的指向;
3.考虑到效率的问题,如果该链表长度无限增加,时间复杂度为O(n)那么查询效率很低,因此我们修改了一下查询方法,我们使用了折半查找算法;
代码实现
public class StrineLinkedList<T> {
/**
* @first,@last
* 整个链表里面的头节点和尾节点;
* @Author:strine
* */
transient Node<T> first;//没有prev节点的就是头节点;
transient Node<T> last;//没有next节点的就是尾节点;
transient int size=0;
/**
*
* 双向链表
* */
private static class Node<T>{
//元素
T item;
//next节点
Node<T> next;
// prev节点
Node<T> prev;
Node(Node<T> prev,T element,Node<T> next){
this.item=element;
this.prev=prev;
this.next=next;
}
}
/**
* 新增方法
* */
public void add(T t){
Node<T> l = last;
//采用尾插法
Node<T> newNode = new Node<T>(l,t,null);
last=newNode;
if (l==null){
//说明是第一次新增
first=newNode;
}else {
l.next=newNode;
}
size++;
}
public Node<T> getNode(int index){
// 折半查找算法
if (index<size>>1){
Node<T> x = first;
for (int i = 0; i <index; i++) {
x=x.next;
}
return x;
}else {
Node<T> x = last;
for (int i = size-1; i >index; i--) {
x=x.prev;
}
return x;
}
}
public T get(int index){
Node<T> node = getNode(index);
return node.item;
}
public T remove(int index){
Node<T> node = getNode(index);
T x = node.item;
Node<T> prev = node.prev;
Node<T> next = node.next;
if (prev==null){
first=next;
}else {
prev.next=next;
node.prev=null; // 告诉GC去清理垃圾
}
if (next==null){
last=prev;
}else {
next.prev=prev;
node.next=null;
}
node.item=null;
size--;
return x;
}
单链表常见面试题
1.第一个问题,因为我们已经在链表中维护了size这个属性了,因此只需要返回该size即可;
2.第二个问题我们可以拿到当前链表的尾结点,然后进行反向遍历;
3.第三个问题逻辑稍微有点绕,我们直接来看代码:
public StrineList<T> overTurn(){
StrineList<T> newList = new StrineList<>();
Node<T> la = last;
newList.first=la;
Node<T> newNext = newList.first;
for (int i = size-1; i >= 0; i--) {
Node<T> head = getNode(i);
newNext.next=head;
newNext=head;
newList.size++;
}
return newList;
}
该代码的主要逻辑就是:先获取到原链表的尾节点,然后赋给新链表的头节点,然后从后往前遍历原链表,获取一个节点就把那个节点赋给当前节点的next域;
4.第四个问题就直接从后往前遍历即可,或者使用栈(先进后出原理--后面再细讲)来设计;
单向环形链表&约瑟夫问题
设编号为1,2,3.....n的n个人围成一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m的那个人出列,它的下一位又开始从1开始报数,数到m的那个人又出列,以此类推直到所有人出列,由此产生一个出列编号的序列;
提示:
1.用一个不带头节点的循环链表来解决;
2.先构成一个有n个节点的单循环链表;
3.然后又K节点起从1开始计数,记到m的时候,对应节点从链表中删除;
4.然后再从被删除节点的下一个节点又从1开始计数,直到最后一个节点从链表中删除则算法结束;
我这里没有写删除方法,因此就直接去拿元素,然后存到另外一个单向链表中;
我们首先实现一个环形链表如下:
public class CircleList {
Node<Integer> first;
int size;
public static class Node<Interger>{
Node<Integer> next;
Integer data;
Node(Node<Integer>next,Integer t){
this.next=next;
this.data=t;
}
}
public CircleList( int size) {
this.size = size;
if (size>0){
first=new Node<>(null,1);
}
Node<Integer> head = first;
for (int i = 1; i < size; i++) {
Node<Integer> newNode = new Node<>(null, i);
head.next=newNode;
head=newNode;
}
head.next=first;
}
约瑟夫方法:
public StrineList<Integer> getJosephuList(int k,int m){
//约瑟夫问题,从k开始报数,报数到m出列
int count = 1;
StrineList<Integer> list = new StrineList<>();
int sum = 0;
int i=k;
for ( ; ; i++) {
if (i>size){
i=0;
continue;
}
if (sum==size){
break;
}
if (count==m){
/* 第一次i=2,count=1,m=3没有找到
第二次i=3,count=2.m=3没有找到
第三次i=4,count=3找到了,输出4,K自增为=3,Count重置为1
第四次i=3,count=1,m=3 没有找到
第五次i=4,count=2,m=3没有找到
第六次i=5,count=3,m=3,输出1,K自增为4,Count重置1;
第七次i=4,count=1,m=3
第八次i=5,count=2,m=3 没有找到
第九次,i=0,count=3,m=3 输出1,K自增为5,Count重置1.
第十次,i=5,c=1 m=3
i=0 c=2 m=3
i=1 c=3 m=3 输出2 K=0,Count重置1;
i=0 c=1 m=3
i=1 c=2 m=3
i=2 c=3 m=3 输出3 k=1 count重置1 sum=size 结束循环
*/
Integer ret = get(i);
list.put(ret);
count=1;
k=k+1;
i=k;
if (k==size){
k=0;
}
sum++;
}
count++;
}
return list;
}
如果是删除的话,则在找到目标节点之后调用删除方法即可,直到原链表为空才结束循环
(这里我偷了个懒,没有实现删除方法,因此直接将找到的元素存到了单链表中,直到新链表的长度和原链表的长度相同则结束循环);