Review
数据结构【树和二叉、图】 — java集合类巩固, 链表专题arithmetic — 分几篇专题
中间插了一节Linux,Linux还是很重要的,当然博主后期还会继续补充计算机网络、操作系统相关的知识,同时最主要的是SQL,包括Mysql的复习和noSQL的代表Redis
个人经过几天的试水,发现项目还是写后端吧,前端的CSS真的emmm… 反正短时间内本人的项目都只负责后端模块;前端只是能看懂,让封装一个简单的组件还行,复杂的就算了…
另外arithmetic感觉链表的题真的没啥难度,主要就是思想要到位,还没有KMP、滑动的一半难
data structure
前面的review中已经就分享过线性表了,但是一笔带过了,这里结合之后的链表专题来总结一下,同时会结合java的类库来进行实现
链表
首先还是简单介绍一下书本上定义的链表结果,书本当然是基于C++实现的,当时也用C++比较顺手
-
单链表: 链表是一种通过指针串联再一个的线性结构,每一个数据结点都是两部分组成,一种数据域一个指针域(存放指向下一个结点的指针),最后一个结点的指针域指向null,空指针
-
双链表: 每一个节点都有两个指针域、一个指向下一个节点,一个指向上一个结点。 双链表既可以向前查询也可以向后查询 【 但是现在arithmetic中一般挺少】
-
循环链表: 循环链表可以解决约瑟夫环问题 【高频问题】,还有就是普通的单链表的判环
存储方式
链表既然是链式的,所以内存地址不是连续分布的,而是散乱分布在内存中的地址中,散乱分布在内存中的地址上,分配机制取决于操作系统的内存管理【 地址分散,通过指针串联在一起】
链表操作
删除结点: 只要将结点的next指向下一个结点的next就可以了,如果是在C++中,是没有JVM的GC垃圾回收的,所以需要free进行手动的释放内存,所以最好用一个指针指向这块地址,不然找不到了【java中可以内存回收,不用手动释放】
添加结点: 链表适合做增加和删除,复杂度都是O(1),比顺序表好多了; 但是注意如果是按位置进行添加,那么就需要进行遍历,new 一个结点然后将其插入到其中即可【 创建一个链表的方法头插法和尾插法,C++中经常用,这里就不再赘述】, 尾插法是逆序过来的 — 所以反转链表的另外一个方法就是创建一个dummyHead进行头插即可
java中的LinkedList
java的集合后面会单独出一篇博客来安利一下,因为真的很常用,特别是用java来刷题的朋友们,选一个集合很重要,并且一般都是没有提示的,所以需要记住常用的方法(比如Set的add,contains)、Map的containsKey,get,put、getOrDefault、List的add、contains,但是注意其remove方法的参数是index,所以不能使用List来做频度序列的数据结构-----(建议使用频度数组)— 具体的技巧可以上加也可以下埋
- java的ArrayList和LinkedList的区别?
很easy,首先ArrayList和LinkedList都实现了List接口,都是线性的数据结构,但是ArrayList底层是基于数组的,所以适合进行随机存取,增删的效率低; LinkedList是基于双向链表实现的,更适合做增删的操作; 需要注意的是LinkedList同时实现了Deque ,可以当作双向队列使用,这是ArrayList不具备的
- LinkedList和ArrayList线程不安全,如何?
LinkedList和ArrayList都是线程不安全的,因为多个线程竞争的时候,可能导致数据的添加的不准确,还有就是扩容不正确【CPU是随机分配资源给线程,所以可能线程执行到一般的时候失去了控制权 ---- 这个后面操作系统会讲一下】
提供三种解决办法: concurrent 同时发生地,并存的
- 使用集合工具类Collections【Colloection是接口 – 集合总】,该类的类方法synchronizedList包裹一下就可以是线程安全的了,包装之后就加上了同步锁就安全了
- 使用安全的集合类来进行代替,比如CopyOnWriteArrayList【CopyOnWriteArrayList是线程安全的,实例方法中加上了对象锁🔒,两种线程 – write线程和read线程;write线程在操作的时候都会将原来的数组给复制一份副本,写线程操作的都是副本,操作完成之后将复制给原数组,所以不影响读操作, 只是write线程会竞争 ----- Copy为主,但是问题就是Copy的不是最新的数据,并且每次复制会导致空间的占用效率低】ConcurrentLinkedQueue也是线程安全的
- 使用Vector 【方法加上了synchronized关键字】
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{ //实现了List接口,也是一个列表,同时实现了序列化用于网络传输,序列化还可以通过另外方法ObjectInputStream的readObject和对应的writeObject也可以 在Dubbo中,provider的实体bean定义在中间接口工程中,因为要进行IO网络传输,所以必须序列化,实现Serializable
public synchronized void removeElementAt(int index) {
CopyOnWirte有lock锁🔒
并且同时要注意Arrays工具类,这个类还是使用挺频繁的,其排序还是挺快的,sort底层就是简单排序和快排,后面也会解释排序,字符串String的比较是一个字符一个字符比较,也就是字典序排序
class Solution {
public List<Integer> lexicalOrder(int n) {
//首先应该使用java原本的api
String[] test = new String[n];
for(int i = 0 ; i < n ; i ++) {
test[i] = String.valueOf(i + 1);
}
Arrays.sort(test);
List<Integer> res = new ArrayList();
for(int i = 0; i < n ; i ++) {
res.add(Integer.parseInt(test[i]));
}
return res;
}
}
----优化的算法就是手写DFS,非递归的-----字典序就是取模即可,画一棵树即可----
class Solution {
public List<Integer> lexicalOrder(int n) {
List<Integer> res = new ArrayList();
int num = 1; //开始存放
for(int i = 0; i < n ; i ++) {
//一共执行n次,每次放入一个,放入的是number
res.add(num);
if(num * 10 <= n) {
//说明num是下一个字典序的
num *= 10;
}else{
while(num % 10 == 9 || num + 1 > n) { //当找到9或者超出的时候,说明该字典序已经找完,返回上一个中 【DFS】
num /= 10;
}
num ++;
}
}
return res;
}
}
arithmetic
java链表设计
这个还是需要练习一下的,因为之前都是使用的C++来手写的链表,突然换到java还有些蒙圈,但是好了,Java是OOP,所以用OOP的思想,结点就是一个类,其结点的属性都是指针域和数据域,链表也是一个类,链表类中的属性包括头结点和数量size
class ListNode{
int val;
ListNode next;
public ListNode(){}
public ListNode(int val){this.val = val;}
}
public MyLinkedList {
ListNode head; //头节点自定义,所以这里为了方便还是使用dummyHead的原则来简化,只是注意最后返回的是dummyHead.next
int size;
public MyLinkedList(){}
public MyLinkedList(ListNode head, int size) {
head = new ListNode(0); //虚拟的头节点,是否存储看个人想法
size = 0; //初始化
}
//获取链表的第index结点的值;注意看题目,0---index
public int get(int index) {
if(index < 0 || index >= size) {
return -1;
}
ListNode temp = head; //虚拟的头节点
for(int i = 0; i <= index; i ++) { //考虑第0号结点决定=号
p = p.next;
}
return p.val; //排除了dummyHead
}
//在index位置插入
public void addAtIndex(int index,int val) {
if(index < 0) index = 0;
if(index > size) return;
List temp = head;
size ++;
for(int i = 0; i < index; i ++) {
temp = temp.next;
}
ListNode s = new ListNode(val);
s.next = p.next;
p.next = s;
}
//在index位置删除
public void deleteAtIndex(int index) {
if(index < 0 || index >= size) return;
ListNode temp = head;
for(int i = 0; i < index; i++) {
temp = temp.next; //< 找到前一个结点
}
temp.next = temp.next.next;
size --; //注意变化size
}
//在头部插入
public void AddAtHead(int val) {
this.addAtIndex(0,val);
}
public void AddAtTail(int val) {
this.addAtIndex(size, val);
}
}
上面说过,只要链表上手了之后,所有的链表问题都是非常easy的; 这里介绍几个重要的思想方法
-
前排特列【空,1个】
, 防止后面报错空指针 -
虚拟头节点
【 建立在题目中的链表没有头节点的概念-- 也就是第一个结点也是存储的数据】dummyHead -
三小兵方法
【 同时定义三个结点连续操作prev,cur、next — 注意循环条件的位置,出现.都要判断空指针异常 – 所以需要决定定义几个变量】 — 还有两小兵p,q 作用都是为了记录原来结点的位置防止丢失
203. 移除链表元素 - 力扣(LeetCode) (leetcode-cn.com)
熟悉之后这就是一个非常简单的题目,其实注意的就是上面的设计的思路,想要的是目标结点还是目标结点的前一个结点, 如果是目标就是=,不是就是< ,这里类似,使用虚拟头节点方法
判断的条件就是p.next != null 这样找到的就是前面的结点,并且会包含到第一个结点
class Solution {
public ListNode removeElements(ListNode head, int val) {
//虚拟头节点方便
ListNode dummyHead = new ListNode();
dummyHead.next = head;
ListNode p = dummyHead;
while(p.next != null) {
if(p.next.val == val) {
p.next = p.next.next;
}else{
p = p.next;
}
}
return dummyHead.next; //虚拟头节点返回的新链表应该是next开始
}
}
一定要注意dummyHead最后返回的是dummyHead.next,dummy只是虚拟的,这里只是注意一下,逻辑是找到删除,其余情况继续,所以是if —else; 如果不是return,那么就必须分别在块中,return的可以不写else块
206. 反转链表 - 力扣(LeetCode) (leetcode-cn.com)
第一种思路是头插法,这里使用两小兵,使用虚拟结点轻松ak
class Solution {
public ListNode reverseList(ListNode head) {
//第一种思路,头插,使用头节点
ListNode dummyHead = new ListNode(0);
ListNode p = head;
//再来一个小兵防止丢失,q记录下一个结点的位置p.next
//这里需要的是当前结点,所以条件因该是p != null ,不是.next
while(p != null) {
ListNode q = p.next;
//插入
p.next = dummyHead.next;
dummyHead.next = p;
//继续移动,q记录的位置
p = q;
}
return dummyHead.next;
}
}
返回dummyHead.next
还有一种思路就是就地反转呢; 这里因为就地反转同时操作的是两个结点,所以需要三小兵prev,cur,next; 想这种next = cur.next----- 本来就是循环 就放在循环中就可以了,以cur为主呢
class Solution {
public ListNode reverseList(ListNode head) {
//还有一种思路,就是就地反转,就是前面来一个虚拟的结点dummyHead = null,因为原本的尾结点也是null
ListNode dummyHead = null; //呼应呢
//这里两个小兵不够,因为同时操作的两个结点,来三个
ListNode prev = dummyHead;
ListNode cur = head;
while(cur != null) {
ListNode next = cur.next;
//操作
cur.next = prev;
//移动
prev = cur;
cur = next; //所以最后prev在最后一个结点
}
return prev;
}
}
只是需要最后的头节点的位置是prev,prev移动了最后
24. 两两交换链表中的节点 - 力扣(LeetCode) (leetcode-cn.com)
还是虚拟结点和三小兵, 这里的三小兵中的next和cur都是prev的小弟,所以都放在循环里面
class Solution {
public ListNode swapPairs(ListNode head) {
//上来就是头节点和三小兵,因为交换涉及两个结点,记录位置还需要一个就是3个
ListNode dummyHead = new ListNode();
dummyHead.next = head;
ListNode prev = dummyHead;
while(prev.next != null && prev.next.next != null) { //懒判断,前面错了,后面就不判断了
ListNode cur = prev.next;
ListNode next = cur.next;
prev.next = next;
cur.next = next.next;
next.next = cur;
//移动到下一个位置
prev = cur; //cur的位置 这里的三小兵也有点假,实际上只是prev在起作用
}
return dummyHead.next;
}
}
返回的是dummyHead.next🎉