线性表
无论是一维数组也好,二维、三维数组也好,本质都是一张表:都是一堆数据元素的排列,只不过二维的每一个元素都是一个一维数组(三维以此类推),那么如何运用好数组呢?也是一门学问——见稀疏数组,就是数组压缩的例子之一。
链表也是线性表,只不过存储方式是链式的,即在计算机中不是连续存储的。
队列和栈,都是操作受限的线性表,可以有顺序存储结构和链式存储结构。队列强调先进先出,只允许队首元素deQueue、队尾元素enQueue(双向除外);栈强调后进先出,只允许在栈顶push和pop,不允许操作、访问栈底元素(双向除外)。
哈希表融数组+链表 或 未来的 数组+二叉树 于一体,严格意义上应该不能在这个概论里说吧……不过数组+链表的结合还是可以放在这里说,它通过键值对的形式存放和快速查找某元素,主要体现了查询的迅速。
稀疏数组
思路分析:
当一个数组中大部分元素都为0或者为同一个值时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方式是:
记录数组的行数、列数、有多少个不同的值(或称:非零值(仅大部分元素为0的稀疏数组可称))。
把这些不同值的元素的行、列、值三个为一组记录下来。
代码实现
SparseArray
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| package com.yukinoshita.sparseArray;
import java.lang.reflect.Array;
public class SparseArray { public static void main(String[] args) { System.out.println("原数组:"); int[][] chessArray = new int[11][11]; chessArray[1][2] = 1; chessArray[2][3] = 2; for (int i = 0; i < chessArray.length; i++) { for (int j = 0; j < chessArray[i].length; j++) { System.out.print(chessArray[i][j] + " "); } System.out.println(); } int sum = 0; for (int i = 0; i < chessArray.length; i++) { for (int j = 0; j < chessArray[i].length; j++) { if (chessArray[i][j] != 0) sum++; } }
System.out.println("稀疏数组:"); int[][] spareArray = new int[sum + 1][3]; spareArray[0][0] = 11; spareArray[0][1] = 11; spareArray[0][2] = sum;
int k = 0; for (int i = 0; i < chessArray.length; i++) { for (int j = 0; j < chessArray[i].length; j++) { if (chessArray[i][j] != 0) { k++; spareArray[k][0] = i; spareArray[k][1] = j; spareArray[k][2] = chessArray[i][j]; } } }
for (int i = 0; i < spareArray.length; i++) { for (int j = 0; j < spareArray[i].length; j++) { System.out.print(spareArray[i][j] + " "); } System.out.println(); } System.out.println("还原为二维数组:"); int[][] array = new int[spareArray[0][0]][spareArray[0][1]]; for (int i = 1; i <= spareArray[0][2]; i++) { array[spareArray[i][0]][spareArray[i][1]] = spareArray[i][2]; }
for (int i = 0; i < array.length; i++) { for (int j = 0; j < array[i].length; j++) { System.out.print(array[i][j] + " "); } System.out.println(); } } }
|
链表
单向链表
思路分析
- 链表是以节点的形式链式存储
- 每个节点包含data域、next域
- 链表分带头节点的链表和不带头节点的链表
initList初始化一个空的单链表
insertList插入操作有头插法和尾插法,还可以根据需求在插入时是否按照顺序进行插入
- 先给新节点连接好他的尾部
newNode.setNext(head.getNext())
- 再把新节点插入
head.setNext(newNode)
deleteList删除操作,要注意判断能否删,可选择传入参数,来对删除元素作限制(例如根据index删、根据data域来删)
traveseList遍历操作,while(p!=null)作为条件,p=p->next;作为迭代条件实现遍历操作。
reverseList单链表的反转-面试题,使用p,next,prev三个指针可实现反转操作。关键步骤为:
- 初始化
p=head或者p=head->next(带头节点写法),总之p为第一个有值的节点;prev=NULL;
- 循环条件
while(p!=NULL)
- 保存下一个节点位置信息
next=p->next
- 反转当前节点指向
p->next=prev
- 将前一节点更新,为下次反转做准备
prev=p。并且更新p=next否则死循环。
- 退出循环时,将头节点位置更新
head=prev或者head->next=prev(带头节点的写法)
代码实现
SingleLinkedList
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| package com.yukinoshita.linkedList;
public class SingleLinkedList { Node head = null;
public void initList() { head = new Node(); }
public void insertList(Node newNode) { newNode.setNext(head.getNext()); head.setNext(newNode); }
public void traverseList() { Node p = head.getNext(); while (p != null) { System.out.printf("%d\t", p.getData()); p = p.getNext(); } System.out.println(); }
public void deleteList() { if (head.getNext() != null) { head = head.getNext(); } }
public void reverseList() { Node p = head.getNext(); Node prev = null; while (p != null) { Node next = p.getNext(); p.setNext(prev); prev = p; p = next; } head.setNext(prev); }
public boolean isEmpty() { return head.getNext() == null; } }
class Node { private int data; private Node next;
public Node() { this.data = 0; this.next = null; }
public Node(int data) { this.data = data; this.next = null; }
public int getData() { return data; }
public void setData(int data) { this.data = data; }
public Node getNext() { return next; }
public void setNext(Node next) { this.next = next; }
@Override public String toString() { return "data=" + data; } }
|
SingleLinkedListDemo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| package com.yukinoshita.linkedList;
import java.util.Scanner;
public class SingleLinkedListDemo { public static void main(String[] args) { SingleLinkedList list = new SingleLinkedList();
int operate = -1; Scanner scanner = new Scanner(System.in); while (true) { System.out.println(""" ------------------------------------- 0:退出程序 1:initList初始化链表操作 2:traverseList遍历并打印链表操作 3:insertList插入链表新节点 4:deleteList删除节点 5:reverseList反转链表 ------------------------------------- """); System.out.print("Enter your choice: "); operate = scanner.nextInt(); switch (operate) { case 0 -> { System.out.println("程序退出"); scanner.close(); System.exit(0); } case 1 -> { System.out.println("链表初始化"); list.initList(); } case 2 -> { if (list.isEmpty()) { System.out.println("当前链表为空,请先初始化"); } else { System.out.println("链表元素为:"); list.traverseList(); } } case 3 -> { if (list.isEmpty()) { System.out.println("当前链表为空,请先初始化"); } else { System.out.print("建立节点(先输入data):"); Node newNode = new Node(scanner.nextInt()); list.insertList(newNode); } } case 4 -> { if (list.isEmpty()) { System.out.println("当前链表为空,请先初始化"); } else { System.out.println("删除节点:"); list.deleteList(); } } case 5 -> { if (list.isEmpty()) { System.out.println("当前链表为空,请先初始化"); } else { System.out.println("反转链表:"); list.reverseList(); } } } } } }
|
双向链表
思路分析
对比单向链表:
- 查找更自由,可以从头或尾找起
- 删除更自由,可以实现自我删除,而单链表必须是找到待删除节点的前一个节点。
- 双向链表的节点中除了data域、next域,还有prev域
traverseList遍历操作与单向链表一致,只是更加自由(可以额外接受参数,实现从前OR后查找)
insertList无非是新增了prev域,但是具体实现还是要仔细!
deleteList无非是新涉及了prev域的操作,并且删除时可以自我删除。
总体来说双向链表的实现很简单,在单向的基础上多考虑一个prev域就行。
代码实现
DoubleLinkedList
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| package com.yukinoshita.doubleLinkedList;
public class DoubleLinkedList { Node head = null; Node tail = null;
public void initList() { head = new Node(); tail = head; }
public void insertList(Node newNode) { newNode.setNext(tail.getNext()); newNode.setPrev(tail); tail.setNext(newNode); tail = newNode; }
public void traverseList() { Node p = head.getNext(); while (p != null) { System.out.printf("%d\t", p.getData()); p = p.getNext(); } System.out.println(); }
public void deleteList() { if (head.getNext() != null) { head.getNext().setPrev(null); head = head.getNext(); } }
public boolean isEmpty() { return head == null; } }
class Node { private int data; private Node next; private Node prev;
public Node() { this.data = 0; this.next = null; this.prev = null; }
public Node(int data) { this.data = data; this.next = null; this.prev = null; }
public int getData() { return data; }
public void setData(int data) { this.data = data; }
public Node getPrev() { return prev; }
public void setPrev(Node prev) { this.prev = prev; }
public Node getNext() { return next; }
public void setNext(Node next) { this.next = next; }
}
|
DoubleLinkedListDemo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| package com.yukinoshita.doubleLinkedList;
import java.util.Scanner;
public class DoubleLinkedListDemo { public static void main(String[] args) { DoubleLinkedList list = new DoubleLinkedList();
int operate = -1; Scanner scanner = new Scanner(System.in); while (true) { System.out.println(""" ------------------------------------- 0:退出程序 1:initList初始化链表操作 2:traverseList遍历并打印链表操作 3:insertList插入链表新节点 4:deleteList删除节点 ------------------------------------- """); System.out.print("Enter your choice: "); operate = scanner.nextInt(); switch (operate) { case 0 -> { System.out.println("程序退出"); scanner.close(); System.exit(0); } case 1 -> { System.out.println("链表初始化"); list.initList(); } case 2 -> { if (list.isEmpty()) { System.out.println("当前链表为空,请先初始化"); } else { System.out.println("链表元素为:"); list.traverseList(); } } case 3 -> { if (list.isEmpty()) { System.out.println("当前链表为空,请先初始化"); } else { System.out.print("建立节点(先输入data):"); Node newNode = new Node(scanner.nextInt()); list.insertList(newNode); } } case 4 -> { if (list.isEmpty()) { System.out.println("当前链表为空,请先初始化"); } else { System.out.println("删除节点:"); list.deleteList(); } } } } } }
|
单向环形/循环链表
思路分析
有助于解决约瑟夫问题
构建一个单向环形链表的思路:
- 先创建第一个节点,让first指向该节点,并形成环形。
- 后面每创建一个新的节点,就加入到已有的环形链表(画图理解)
traverseList当p.getNext() == first说明已完成遍历
代码实现
这里的单向环形链表的部分方法(deleteList操作)是针对于约瑟夫问题特化的
JosephuSolved
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package com.yukinoshita.dataStructure.singleCircleLinkedList;
public class JosephuSolved { public static void main(String[] args) { SingleCircleLinkedList list = new SingleCircleLinkedList(); list.initList();
for(int i=0;i<5;i++){ list.insertList(i+1); }
list.traverseList();
while(!list.isEmpty()){ System.out.printf("%d\t",list.deleteList(3)); } }
}
|
SingleCircleLinkedList
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| package com.yukinoshita.dataStructure.singleCircleLinkedList;
public class SingleCircleLinkedList { private Node first; private Node last; public void initList(){ first = null; last = null; }
public boolean isEmpty(){ return first == null; }
public void insertList(int data){ if(isEmpty()){ first = new Node(data,first); last = first; return; } Node newNode = new Node(data,first); last.setNext(newNode); last = newNode; }
public int deleteList(int index){ Node prev = last; Node p = first; if(first.getNext() == first){ first = null; last = null; return p.getNum(); } for (int i = 0; i < index - 1; i++) { prev = p; p=p.getNext(); }
prev.setNext(p.getNext()); last = prev; first = p.getNext();
return p.getNum(); }
public void traverseList(){ if(isEmpty()){ System.out.println("链表为空"); return; } Node p = first; do{ System.out.printf("%d\t",p.getNum()); p=p.getNext(); }while(p!=first); System.out.println(); } }
class Node{ private int num; private Node next;
public Node(int num,Node next){ this.num = num; this.next = next; }
public int getNum() { return num; }
public void setNum(int num) { this.num = num; }
public Node getNext() { return next; }
public void setNext(Node next) { this.next = next; } }
|
链表题目
查找单链表的倒数第k个节点
- 编写一个方法,接收head节点,同时接收一个index
- index表示倒数第index个节点
- 若
index<0或者index>length,则查找不到。
- 如果在设计单链表时,携带length属性(insert时++、delete时–、有get方法),直接获取总长度,遍历后即可到达第
length-index+1个节点,即到时第k个节点。若没有length,则需要先遍历一遍获得单链表总长度。(毕竟是单链表,只能从前往后遍历,所以需要预先知道总长才能查询到倒数第k个)
单链表的反转
见单向链表的思路分析部分reverseList。
从尾到头打印单链表
- 方式一:先reverse,再打印,但是破坏了原来的结构(不推荐)
- 方式二:把节点压入栈中,利用栈后进先出的的特点,实现逆序打印
合并两个有序的单链表,合并后仍保持有序
- 两个单链表都还没遍历完,就比大小后再插入
- 有一个为空了,剩下的那个直接接入到最后即可