链表
链表的结构非常像火车,有车厢、有连接车厢的挂钩
车厢里存储的数据,每个车厢都有座位,还有有一个挂钩,用于连接另一节车厢,而这些车厢连在一起就是一整个火车
同样的,对于链表来说,存在若干个结点Node,结点中存储这数据和下个结点的地址,将这些Node连接在一起的整体就是链表
现在要来设置一下Node:
class Node{
int data;//以int型为例
Node next;
}
车厢 - Node已经有了,而对于用户来说,用户在意的并不是某一节车厢,而是哪一个车次,因此我们引入一个头结点,通过头结点就能过找到后续所有的结点
用户只需要调用单链表提供的增删改查方法即可,不需要关心链表内部是如何实现的,就像坐火车,你只管入座,火车怎么驾驶,车厢之间怎么连接都不需要关心,更不需要你去做
根据上面两点,就可以定义链表:
public class SingleLinkedList{
Node head;//头结点 - 有了头结点就能通过头结点找到整个链表
int size;//记录元素个数
public String toString(){
int cur = head;
String ret = "";
while(cur != null){
ret += cur.data;
ret += "->";
cur = cur.next;
}
ret += "NULL";
}
}
增加
void addFirst(int val)
-> 在链表头部插入结点
void add(int index, int val)
-> 在链表索引为index处插入元素值为val的结点
void addLast(int val)
-> 在链表尾部插入结点
除了链表的头结点,其他结点都是中间位置,包括尾结点,因此处理头结点的时候要单独处理
因为只有头结点没有前驱,其他的结点都有前驱结点,所以找到前驱结点至关重要
//头插法
public void addFirst(int val){
Node node = new Node(val);
if(head != null){
node.next = head;
}
head = node;
size++;
}
//在index处插入
public void addIndex(int index, int val){
if(index < 0 || index > size){
System.out.println("add index is illegal");
return;
}
//头结点没有前驱,因此需要特殊处理
if(index == 0){
addFirst(val);
return;
}
//在单链表中最核心的部分就是找到前驱,因为我们需把新结点的地址赋值给前驱的next属性
Node prev = head;
//从头结点开始,找到索引为index的结点的前驱,需要走index - 1步
for (int i = 0; i < index - 1; i++) {
prev = prev.next;
}
Node node = new Node(val);
//下面两步的操作顺序不能变,否则node.next就指向自己了,而后面的结点将丢失
node.next = prev.next;
prev.next = node;
size++;
}
//尾插法
public void addLast(int val){
//调用我们写好的代码即可
addIndex(size, val);//size正好是我们需要插入的位置
}
我们观察一下链表和顺序表的add方法
如果把动态数组换成链表,对于用户来说,是非常容易的,只需要更换类即可,调用的方法,和使用的规则完全一致
而这就是面向对象开发的魅力所在
查找
int getByValue(int val)
-> 查找索引为index的元素值是多少
boolean contains(int val)
-> 查询是否包含元素值为val的结点
int getByIndex(int index)
-> 查找链表中第一个值为val的结点索引是多少
实际上,不管是查找、修改还是删除,都需要判断index的合法性,且这三种数据操作的index的取值都在**[ 0, size )**之间
既然如此,我们直接将index的检验抽象成一个方法,并且定义成私有方法,只供程序内部自己使用的检验方法,用户不需要调用
//在链表中找到值为val的元素,并返回下标,否则返回-1
public int getByval(int val){
Node node = head;
int num = 0;
while(node != null){
if(node.data == val){
return num;
}
num++;
node = node.next;
}
return -1;
}
//第二种写法
public int getByval(int val){
int index = 0;
for (Node node = head; node != null; i++) {
if(node.data == val){
return i;
}
node = node.next;
}
return -1;
}
//查找链表中是否包含val
public boolean contains(int val){
return getByval(val) != -1;
}
//查找链表中,下标为index的元素值
public int getByIndex(int index){
if(!indexLegal(index)){
System.out.println("get index is illegal");
return -1;
}
Node node = head;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node.data;
}
修改
int sex(int index, int newVal)
-> 在索引值为index处插入元素值newVal,并返回修改前的元素
//将下标为index的结点的值修改为newVal,并返回旧的值
public int set(int index, int newVal){
if(!indexLegal(index)){
System.out.println("set index is illegal");
return -1;
}
Node node = head;
for (int i = 0; i < index; i++) {
node = node.next;
}
int oldVal = node.data;
node.data = newVal;
return oldVal;
}
删除
remove(int index)
-> 删除索引为index位置的元素
removeValueOnce(int val)
-> 删除第一个值为val的元素
removeAllValue(int val)
-> 删除链表中所有值为val的元素
删除元素值为val的结点的时候,尤其要注意我们要找到的是待删除结点的前驱
//删除索引为index位置的元素,返回被删除的元素值
public int remove(int index){
if(!indexLegal(index)){
System.out.println("remove index illegal");
return -1;
}
//头结点没有前驱,需要特殊处理
if(index == 0){
Node prev = head;
head = head.next;
prev.next = null;
size--;
return prev.data;
} else {
Node prev = head;
for (int i = 0; i < index - 1; i++) {
prev = prev.next;
}
Node node = prev.next;
prev.next = node.next;
node.next = null;
size--;
return node.data;
}
}
//删除链表中第一个值为val的节点,返回是否删除成功
public void removeValOnce(int val){
if(head == null){
System.out.println("LinkedList is empty");
}
//头结点就是待删除元素
if(head.data == val){
Node x = head;
head = x.next;
x.next = null;
size--;
return;
}
Node prev = head;
//因为prev = head,所以此时的prev一定不是待删除结点,如果是的话,在上面的代码中就已经处理过了
//因此,我们判断结点是否等于待删除结点,至少也要从当前结点的下一个结点开始判断,即prev.next.data
//那么为了保证data的存在,我们就要保证prev.next的存在,因此whil的循环条件就是prev.next != null
while(prev.next != null){
if(prev.next.data == val){
Node x = prev.next;
prev.next = x.next;
x.next = null;
}
prev = prev.next;
}
System.out.println("val is not existing");
}
//删除链表中所有值为val的结点
public void removeAllVal(int val){
//如果头结点是待删除元素,新的头结点可能还是待删除元素,因此要用while循环
//其次,如果整个链表都是待删除结点,head最终会取null,如果继续运行会报空指针异常
while(head != null && head.data == val){
Node x = head;
head = x.next;
x.next = null;
size--;
}
//如果整个链表都已经删完了,直接退出
if(head == null){
return;
}
Node prev = head;
while(prev.next != null){
if(prev.next.data == val){
Node x = prev.next;
prev.next = x.next;
x.next = null;
size--;
}else{
prev = prev.next;
}
}
}
//删除头结点
public void removeFirst(){
remove(0);
}
//删除尾结点
public void removeLast(){
remove(size - 1);
}