1.链表类和节点的定义问题
在链表中存储元素,最终放在节点类,链表是由一系列的节点组成的,最终用户只需要和链表类打交道即可,至于元素到底在那个储存,怎么储存,用户不关心的,Node类对用户完全隐藏。
单向链表:此链表只能从第一个节点开始向后遍历,无法从后向前。
不带头:看第一个节点是否保存具体的元素。
// 节点类
class Node{
public int val;// 默认值为0
public Node next;// 每个节点都保存下一个节点的地址,若下个节点不存在,则默认值为null,当链表的某个节点的next为null,则该节点为链表的尾节点。
public Node(int val){
this.data = val;
this.next = null;
}
}
//头插法
头插需要注意,步骤1 和 步骤2 必须按顺序执行,不能交换。如果交换会让新节点先挂载在当前链表的头部。然后再移动head指向新节点。效果:最终head指向了当前的新节点node自己指向自己。
//头插法
public void addFirst(int val){
Node node = new Node(val);
if (head == null){
head = node;//步骤 1
head.val = node.val;//步骤 2
head.next = null;
return;
}
node.next = head;
head = node;
}
//尾插
尾插需要找到从head开始找到尾节点tail,找前驱点,需要注意不能直接用head引用去找,因为在链表类中需要使用head保证第一个节点的地址,若直接使用head操作,操作之后第一个节点在哪里就不知道了。
// 尾插
public void add(int val) {
Node node = new Node(val);
if (head == null){
head = node;
size++;
return;
}
Node tail = head.next;
while (tail.next != null){
tail = tail.next;
}// tail一定指向当前链表的尾节点
tail.next = node;
node.next = null;
size++;
}
//在索引为index的位置插入新的元素 例如 index 2,val 10
插入新的节点元素步骤,1 产生新的节点,将10放入 2 要找到待插入位置的前驱节点prev (前驱节点位置:从head开始走index-1步,就走到了待插入的前驱位置)3 将新节点和原链表关联起来
public void add(int index, int val) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("add index illegal!");
}
// 2.头插
if (index == 0) {
addFirst(val);
return;
}
//任意位置插入
Node prev = head;
Node node = new Node(val);
for (int i = 0; i < index - 1; i++) {
prev = prev.next;
}
node.next = prev.next;
prev.next = node;
size++;
}
//查找是否含关键字val是否在链表
//查找是否含关键字val是否在链表
public boolean contains(int val) {
if (head == null) {
return false;
}
Node cur = head;
while (cur.next != null){
if (cur.val == val){
return true;
}
cur = cur.next;
}
return false;
}
//返回索引为index的元素值
//返回索引为index的元素值
public int get(int index) {
if (!RangeCheck(index)){
throw new IllegalArgumentException("get new illegal ");
}
Node cur = head;
for (int i = 0;i < index;i++){
cur = cur.next;
}
return cur.val;
}
//查找是否包含关键字val是否在单链表当中
public boolean contains(int val){
Node cur = head ;
if (head == null){
return false;
}
while (cur != null){
if (cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
//删除第一次出现关键字为val的节点
在单链表,无论是插入还是删除,都的找前驱!还要考虑头结点的情况。
做题删除其中的节点,我们只关心能否访问就行了,因为JVM判断一个空间是否要回收,空间内部是否包含强引用(JVM)
public void removeByValueOnce(int val) {
// 1.base case 判断链表是否为空
if (head == null) {
return;
}
// 2.判断头节点就是待删除的节点
if (head.val == val) {
Node next = head.next;
head.next = null;
head = next;
size --;
return;
}
// 3.此时头节点一定不是待删除的节点,且不为空!
Node prev = head;
while (prev.next != null) {
if (prev.next.val == val) {
// 说明此时prev恰好就是待删除节点的前驱
// cur就是待删除的节点
Node cur = prev.next;
prev.next = cur.next;
cur.next = null;
size --;
return;
}
// 此时prev不是待删除节点的前驱,继续向后判断
prev = prev.next;
}
}
//删除所有值为val的节点
需要注意prev什么时候移动
public void removeAllValue(int val) {
// 1.base case
if (head == null) {
return;
}
// 2.判断头节点就是待删除的节点
while (head != null && head.val == val) {
Node next = head.next;
head.next = null;
head = next;
size --;
}
// 3.代码走到这里有哪几种情况?
// 头节点已经处理"完毕"了 =>
// 第一种可能 head.val != val 头节点还存在,但是值不是待删除的值
// [3,3,3,3,5,3] => [5,3] head == 5
// 第二种可能,链表为空了~
// [3,3,3,3] => [] head == null
if (head == null) {
// 已经把链表删完了
return;
}
// 4.此时保证prev一定不为空
Node prev = head;
// 现在咱们要向后看。需要调用prev.next => 但是此时要保证prev不为空
while (prev.next != null) {
Node cur = prev.next;
if (cur.val == val) {
// 此时prev走到了待删除的节点前驱
prev.next = cur.next;
size --;
cur.next = null;
}else {
// 只有当下一个节点不是待删除节点时,才能移动prev引用
prev = prev.next;
}
}
}
//删除单链表索引为Index位置的节点
public int removeByIndex(int index) {
// 1.index合法性校验
if (!rangCheck(index)) {
throw new IllegalArgumentException("remove index illegal!");
}
// 2.判断若index == 0,即删除头节点的情况
if (index == 0) {
Node next = head.next;
int val = head.val;
head.next = null;
head = next;
// 更新ize属性值
size --;
return val;
}
// 3.index确实是中间位置的元素,需要找到待删除位置的前驱节点
Node prev = head;
for (int i = 0; i < index - 1; i++) {
prev = prev.next;
}
// prev就走到了待删除位置index的前驱节点
// cur就是待删除的节点
Node cur = prev.next;
prev.next = cur.next;
cur.next = null;
size --;
return cur.val;
}
}
//得到单链表的长度
//得到单链表的长度
public int size(){
if (head == null){
return 0;
}
Node cur = head;
while (cur != null){
size ++;
cur = cur.next;
}
return size;
}
//链表的遍历
public void display(){
if (head == null){
return;
}
Node cur = head;
while (cur != null){
System.out.println(cur.data);
cur = cur.next;
}
}
//单链表的String的代码
public String toString() {
StringBuilder sb = new StringBuilder();
// 链表的遍历只能从头结点开始依次向后移动直到尾结点
// 此处能否直接使用head进行遍历?不能,若直接用head进行遍历,遍历一次之后,头结点的地址就没了~(我三级头呢?)
for (Node x = head;x != null;x = x.next) {
sb.append(x.val);
sb.append("->");
// 若此时x走到尾结点,拼接NULL
if (x.next == null) {
sb.append("NULL");
}
}
return sb.toString();
//单链表的索引范围判断
private boolean rangCheck(int index) {
if (index < 0 || index >= size) {
return false;
}
return true;
}
在单链表部分的插入和删除,其实关心前驱节点的情况,不用考虑后驱节点是否存在