线性表
线性表是n个具有相同特性的的数据元素的有限序列。常见的线性表有:顺序表、链表、栈、队列…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
定义线性表接口
顺序表、链表等可以通过实现线性表接口来定义增删改查方法。
// 线性表接口定义
public interface SeqList<E> {
// 默认尾插
void add(E element);
// 在线性表中插入新元素,插入后的元素下标为index
void add(int index,E element);
// 删除当前线性表中索引为index的元素,返回删除的元素值
E removeByIndex(int index);
// 删除当前线性表中第一个值为element的元素
void removeByValue(E element);
// 删除当前线性表中所有值为element的元素
void removeAllValue(E element);
// 将当前线性表中index位置的元素替换为element,返回替换前的元素值
E set(int index,E element);
E get(int index);
boolean contains(E element);
// 查询当前线性表中第一个值为element的下标
int indexOf(E element);
}
顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
JDK的原生数组有一个最大的特点,就是一旦长度定义之后,长度固定不变。所以引入动态数组的概念,就是在原生数组的基础上,长度不限定,本质上还是数组。当元素满了之后,动态数组的类就会自动进行扩容处理。
ArrayList就是一个动态数组,是一种基于数组实现的顺序表。
顺序表的初始化
private Object[] elementData;
//保存当前线性表中真正保存的有效元素个数
private int size;
//定义一个无参构造,初始化size的值
public MyArray() {
this(10);
}
//外部传入一个数组进行初始化操作
public MyArray(int size){
this.elementData = new Object[size];
}
//对内部数组做扩容处理
private void grow(){
int oldLength = elementData.length;
//默认扩容为原来的一倍
int newLength = oldLength << 1;
//将原数组中的所有内容拷贝到新数组中,超出的部分使用默认值填充
Object[] newArray = Arrays.copyOf(elementData,newLength);
elementData = newArray;
}
//判断索引的合法性
public boolean rangeCheck(int index){
if(index < 0 || index >= size){
return false;
}
return true;
}
顺序表尾插
public void add(E element) {
this.elementData[size] = element;
size++;
if(size==elementData.length){
//当前数组已满
grow();
}
}
顺序表按索引插入元素
public void add(E element, int index) {
if(index < 0 || index > size){
throw new IllegalArgumentException("add index illegal");
}
//当inde==size时,相当于尾插元素
if(index == size){
add(element);
return;
}
//此时0<index<size
for (int i = size - 1; i < 0; i--) {
elementData[i + 1] = elementData[i];
}
//此时index位置元素空出
elementData[index] = element;
//不能忘记size的大小要加1
size++;
if(size == elementData.length){
grow();
}
}
顺序表按索引删除
public E removeIndex(int index) {
//检验索引的合法性
if(!rangeCheck(index)){
throw new IllegalArgumentException("index is illegal");
}
E oldVal = (E) elementData[index];
//从index位置元素开始,将后一个元素覆盖掉前一个元素
//为了保证索引无越界的可能,index + 1 < size
for (int i = index; i < size - 1; i++) {
elementData[i] = elementData[i + 1];
}
//维护size属性,最后一个位置的元素不妨碍使用,在下一次添加元素时就会将其覆盖
size--;
return oldVal;
}
顺序表按值删除
public void removeByValue(E element) {
for (int i = 0; i < size; i++) {
if(elementData[i].equals(element)){
//碰到第一个待删除的元素
removeIndex(i);
//直接返回
return;
}
}
System.out.println("没有待删除的元素");
}
顺序表删除所有指定值的元素
public void renmoveAllValue(E element) {
//从前往后遍历
for (int i = 0; i < size;) {
if(elementData[i].equals(element)){
//当i指向的是待删除元素时,不能移动i
removeIndex(i);
}else{
//当i指向的不是待删除元素时,i++
i++;
}
}
}
顺序表修改元素
public E set(int index, E element) {
//检查索引的合法性
if(!rangeCheck(index)){
throw new IllegalArgumentException("index is illegal");
}
E oldVal = (E) elementData[index];
elementData[index] = element;
return oldVal;
}
顺序表得到元素
public E get(int index) {
if(!rangeCheck(index)){
throw new IllegalArgumentException("index is illegal");
}
E val = (E) elementData[index];
return val;
}
顺序表查询元素
public int indexOf(E element){
for (int i = 0; i < size; i++) {
if(elementData.equals(element)){
return i;
}
}
throw new NoSuchElementException("element is not exist");
}
顺序表查看是否包含特定元素
public boolean contains(E element) {
// foreach会遍历所有的数组元素,包括无效元素!
// 无效元素此时为null
// for (Object e : elementData) {
// if (e.equals(element)) {
// return true;
// }
// }
for (int i = 0; i < elementData.length; i++) {
if(elementData[i].equals(element)){
return true;
}
}
return false;
}
顺序表覆写toString方法
public String toString() {
StringBuilder sb = new StringBuilder("[");
// 遍历elementData数组
for (int i = 0; i < size; i++) {
sb.append(elementData[i]);
if (i < size - 1) {
sb.append(", ");
}
}
sb.append("]");
return sb.toString();
}
链表
在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后
搬移,时间复杂度为O(n),效率比较低,因此ArrayList不适合做任意位置插入和删除比较多的场景。因此,java集合中又引入了LinkedList,即链表结构。
链表是一种逻辑上连续,物理上不连续的线性表。
不带头单向链表
初始化
//定义第一节车厢的地址
private Node head;
//定义车箱节点个数,也就是保存元素个数
private int size;
//定义一个车厢内,属于火车的私有内部类,对外完全隐藏
private class Node{
//保存的元素
E val;
//下一节车厢的地址
Node next;
Node(E val){
this.val = val;
}
}
头插
//在当前链表头部添加元素
public void addFirst(E val) {
//要插入一个元素,首先要产生一个新节点
Node node = new Node(val);
//将当前节点插入到链表中
node.next = head;
//更新头节点
head = node;
size++;
}
任意位置插入节点
public void add(E element, int index) {
//1、边界条件的判断
if(index<0 || index< size){
throw new IllegalArgumentException("index is error");
}
//2、判断有没有前驱节点
if(index==0){
addFirst(element);
return;
}
//3、确实是中间位置插入,寻找待插入位置的前驱节点
Node prev = head;
for (int i = 0; i < index - 1 ; i++) {
prev = prev.next;
}
//4、prev走到待插入位置的前驱
Node node = new Node(element);
node.next = prev.next;
prev.next = node;
size ++;
}
按索引删除节点
public E removeIndex(int index) {
//1、判断索引是否越界
if(!rangeCheck(index)){
throw new IllegalArgumentException("index is illegal");
}
//2、如果要移除头节点
if(index==0){
Node node = head;
head = head.next;
node.next = null;
size--;
return node.val;
}
//3、此时删除的是中间位置节点
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.val;
}
按值删除节点
public void removeByValue(E element) {
//1、判断链表是否为空
if(head == null){
return;
}
//2、判断头节点恰好是需要删除的节点
if(head.val.equals(element)){
head = head.next;
size--;
return;
}
//3、此时头节点不为空,且一定不是待删除的节点
Node prev = head;
while(prev.next!=null){
if(prev.next.val.equals(element)){
prev.next = prev.next.next;
size--;
return;
}
//注意更新
prev = prev.next;
}
//4、此时链表中没有待删除的值
System.out.println("element is not exist");
}
删除所有指定值的节点
public void renmoveAllValue(E element) {
//1、首先判断链表是否为空
if(head == null){
return;
}
//2、若头节点就是待删除的节点且连续出现待删除的节点
while (head!=null && head.val.equals(element)){
head = head.next;
size --;
}
//3、判断从头节点开始一整个链表都是待删除的节点,会删除完整个链表的情况。
if(head==null){
return;
}
//4、走到这表示头节点一定不是待删除的节点且链表不为null
//此时prev指向的一定不是待删除的节点
Node prev = head;
while (prev.next!=null){
if(prev.next.val.equals(element)){
prev.next = prev.next.next;
size --;
}else{
//只有当后继节点也不是待删除节点时才能移动prev引用!
prev = prev.next;
}
}
}
修改节点
public E set(int index, E element) {
//1、合法性校验
if(!rangeCheck(index)){
throw new IllegalArgumentException("set index illegal");
}
//2、从前向后走走到对应的index位置
Node node = head;
for (int i = 0; i < index; i++) {
node = node.next;
}
//3、此时node就落在了待修改的节点位置
E oldVal = node.val;
node.val = element;
return oldVal;
}
查询节点的值
public E get(int index) {
//1、index的合法性校验
if(!rangeCheck(index)){
throw new IllegalArgumentException("index is illegal");
}
//2、从前往后走走到index位置
Node node = head;
for (int i = 0; i < index; i++) {
node = node.next;
}
//3、返回index位置的值
return node.val;
}
toString方法覆写
public String toString() {
StringBuilder sb = new StringBuilder();
//从当前链表的第一个节点开始遍历,直到尾节点结束
//第一个节点为head
//尾节点的ext值为null
//while循环实现
Node tmp = head;
while (tmp!=null){
//当前tmp还有值没访问
sb.append(tmp.val);
sb.append("->");
if(tmp.next == null){
//此时tmp走到了尾节点
sb.append("NULL");
}
//注意更新tmp
tmp = tmp.next;
}
//for循环实现
for (Node node = head; node!=null ; node = node.next) {
sb.append(node.val);
sb.append("->");
if(node.next == null){
sb.append("NULL");
}
}
return sb.toString();
}
带头单向链表
初始化
// 当前单链表一定存在火车头,且不存储具体元素
private Node dummyHead = new Node(null);
// 具体的元素个数
private int size;
private class Node {
E val;
Node next;
Node(E val) {
this.val = val;
}
}
任意位置插入节点
public void add(int index, E element) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("add index illegal!");
}
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
Node node = new Node(element);
node.next = prev.next;
prev.next = node;
size ++;
}
删除所有指定值的节点
public void removeAllValue(E element) {
// prev一定指向的不是待删除的节点
Node prev = dummyHead;
while (prev.next != null) {
if (prev.next.val.equals(element)) {
prev.next = prev.next.next;
size --;
}else {
prev = prev.next;
}
}
}
递归实现的单向链表
在指定位置插入节点
// 在当前以head为头节点的链表中,在索引index位置插入一个新的值element
// 插入后返回新的链表头结点
private Node addInternal(Node head, int index, E element) {
// 1.base case
if (index == 0) {
// 头插
Node node = new Node(element);
node.next = head;
head = node;
size ++;
return head;
}
// index不在头结点插入
head.next = addInternal(head.next,index - 1,element);
return head;
}
链表值的打印
// 传入一个以head为头节点的链表,就能按顺序输出每个节点的值
private void print(Node head) {
// 1.base case
if (head == null) {
System.out.print("NULL\n");
return;
}
// 已知的只有head
System.out.print(head.val + "->");
// 从第二个节点开始的打印交给子函数
print(head.next);
}
链表值逆序打印
// 传入一个以head为头节点的链表,就能从最后一个节点开始逆序输出链表的每个节点
int count = 0;
private void printReverse(Node head) {
// 1.base case
if (head == null) {
return;
}
count ++;
// 链表有节点 head
// 先将以head.next为头节点的子链表逆序输出之后再输出头节点
printReverse(head.next);
count --;
// 走到这里,可以放心的输出头节点~
System.out.print(head.val + "->");
if (count == 0) {
System.out.print("NULL");
}
}
双向链表
单链表最大的局限在于只能从头遍历到结束,只能从一个节点走到下一个节点,因此,引入双向链表,在任意节点既能向前遍历又能向后遍历。
初始化
private int size; // 有效节点个数
private DoubleNode head; // 头节点
private DoubleNode tail; // 尾结点
private class DoubleNode{
DoubleNode prev;
E val;
DoubleNode next;
public DoubleNode(E val) {
this.val = val;
}
public DoubleNode(DoubleNode prev, E val, DoubleNode next) {
this.prev = prev;
this.val = val;
this.next = next;
}
}
// 根据传入索引与中间位置的关系,决定到底从前向后寻找节点还是从后向前寻找节点
// 内部使用的工具方法
private DoubleNode node(int index) {
DoubleNode result = null;
if (index < (size >> 1)) {
result = head;
for (int i = 0; i < index; i++) {
result = result.next;
}
}else {
result = tail;
for (int i = size - 1; i > index; i--) {
result = result.prev;
}
}
return result;
}
头插
public void addFirst(E element) {
DoubleNode node = new DoubleNode(element);
size ++;
if (tail == null) {
tail = node;
}else {
node.next = head;
head.prev = node;
}
head = node;
}
尾插
public void add(E element) {
DoubleNode node = new DoubleNode(element);
size ++;
if (head == null) {
head = node;
}else {
node.prev = tail;
tail.next = node;
}
tail = node;
}
任意位置插入节点
public void add(int index, E element) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("add index illegal!");
}
if (index == 0) {
addFirst(element);
return;
}
if (index == size) {
add(element);
return;
}
DoubleNode prev = node(index - 1);
DoubleNode next = prev.next;
DoubleNode node = new DoubleNode(element);
// 先处理左边区域
node.prev = prev;
prev.next = node;
// 再处理右半区域
node.next = next;
next.prev = node;
size ++;
}
删除节点
private void unlink(DoubleNode node) {
DoubleNode prev = node.prev;
DoubleNode next = node.next;
// 先处理左半区域
if (prev == null) {
this.head = next;
}else {
node.prev = null;
prev.next = next;
}
// 在处理右半区域
if (next == null) {
this.tail = prev;
}else {
node.next = null;
next.prev = prev;
}
size --;
}
删除索引位置的节点
public E removeByIndex(int index) {
if (!rangeCheck(index)) {
throw new IllegalArgumentException("remove index illegal!");
}
DoubleNode node = node(index);
unlink(node);
return node.val;
}
删除指定值的节点
public void removeByValue(E element) {
DoubleNode node = head;
for (int i = 0; i < size; i++) {
if (node.val.equals(element)) {
unlink(node);
return;
}
node = node.next;
}
}
删除所有指定值的节点
public void removeAllValue(E element) {
DoubleNode node = head;
// 因为每次unlink之后都会修改size的值,但是删除所有元素,
// 要把所有链表节点全部遍历一遍
int length = this.size;
for (int i = 0; i < length; i++) {
DoubleNode next = node.next;
if (node.val.equals(element)) {
unlink(node);
}
node = next;
}
}