♦ 往下进入知识海洋🌊
■ 数据结构篇
● 什么是数据结构?
“数据结构是数据对象,以及存在于该对象的实例和组成实例的数据元素之间的
各种联系
。这些联系可以通过定义相关的函数来给出。”
来源:《数据结构、算法与应用》—Sartaj Sahni
“数据结构是
ADT
(抽象数据类型Abstract Data Type) 的物理实现。”
来源:《数据结构与算法分析》—Clifford A.Shaffer
“数据结构是相互之间存在一种或多种
特定关系
的数据元素的集合。”
来源:《大话数据结构》—程杰
“数据结构是计算机
存储、组织数据
的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。”
来源:彭军、向毅主编.数据结构预算法:人民邮电出版社,2013年
数据结构于我的想法就是,就好比炉石传说里的卡包,每个卡包就是一个数据结构,其中一个卡包结构就包括五张牌,而其中至少包含一张稀有卡,而一张卡牌也是一个数据结构,例如卡牌的数据结构:
class Cards{
private String name; //卡牌名
private String desc; // 描述
private String rarity; // 稀有度 稀有/普通/...
private String type; // 卡牌类型 法术/随从
private int spend; // 费用
//职业等等...
}
当然,这个数据结构比较简单,其实你可以先认为数据结构是存储一类型事物的储物箱,也许储物箱里面还有另一种储物箱,而每个储物箱都有不同的颜色,名称,大小什么的。
● 为什么我们需要数据结构?
数据是程序的核心要素,因此数据结构的价值不言而喻。无论你在写什么程序,你都需要与数据打交道,比如员工工资、股票价格、书籍列表或者联系人信息。在不同场景下,数据需要以特定的方式存储,我们有不同的数据结构可以满足我们的需求。
● 常用的数据结构
数组
,链表
,栈
,队列
,图
,树
,前缀树
,哈希表
其实就是线性表(数组,链表),栈与队列,树与二叉树,图,散列表(hash)
1.数组
数组简单点说就是要你定义一辆火车
,车厢
的多少由你来定,你可以定一节车厢,两节车厢,三节车厢,当然,乘坐过火车的都知道,火车发车了,每个车厢未必满座
,有可能这火车有五节车厢,然而只坐满了三节
,浪费了两节;当然,你可能会说,那以后定三节车厢的火车不就好了,可是这样的话,万一今天刚好放假呢,刚好下班高峰期呢,你的车厢不够,人肯定挤不下
呀,就做到车顶
去了,太危险
了…,那可以把其中一节的车厢换大一点
吗?这是不可以的,因为火车的车厢是要有规范
的,都必须是同一种 类型的车厢
,所以火车的车厢还是要尽量定的大一点,所以数组也挺容易造成空间的浪费
,那有没有可以根据人数多少,人多的话就自动增加车厢
的火车呢?答案是有的,不过在之后才会讲到,期待一下吧。
如何定义一个数组
int[] arrays = new int[5];
这里定义了一个大小为5的int型数组,你可以这样理解成,最左边的int表示你要定义一俩车
,是int型
的车,然后加上[]
后,就表示你要定义有车厢
的火车,火车的名字叫array
,然后new就相当于造车
,造什么车呢,int型
的车,然后加了[5]表示这个车是有五节车厢的火车
,就这样造了一辆五节int车厢的火车,并初始化
每节车厢为0
个人。发现我讲的太慢了…,接下来加快下。
数组常用方法
1.数组的遍历:
int[] arrays = { 1, 2, 3, 4, 5 };
for (int item : arrays) {
System.out.println(item);
}
for (int i = 0; i < arrays.length; i++) {
System.out.println(arrays[i]);
}
打印结果均如下:
1
2
3
4
5
2.数组的打印
可以用上边的循环打印,也可以用Arrays.toString(array)
,将数组转成字符串直接打印,不要直接打印数组
,这样得到的只能是地址。
int[] arrays = {1,2,3,4,5};
String array = Arrays.toString(arrays);
System.out.println(arrays);
System.out.println(array);
打印结果:
直接打印数组: [I@4554617c
转成字符串打印:[1, 2, 3, 4, 5]
3.用数组创建ArrayList
注意
:Arrays.asList(array)转换后的array类型的问题
public class LearnArray {
public static void main(String[] args) {
Integer[] arrays = {1,2,3,4,5};
String array = Arrays.toString(arrays);
ArrayList<Integer> arrayList = new ArrayList<Integer>(Arrays.asList(arrays));
for (Integer a:arrayList){
System.out.println(a);
}
System.out.println(arrayList);
arrayList.add(6);
System.out.println(arrayList);
}
}
打印结果:
1
2
3
4
5
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5, 6]
这里有一个关于Arrays.asList(array)的坑
,就是转换完后的类型是一个不可变长度的列表
,虽然得到的也是一个List,但是相关变长的方法已经不能使用了,来看代码:
public class LearnArray {
public static void main(String[] args) {
Integer[] arrays = {1,2,3,4,5};
String array = Arrays.toString(arrays);
List<Integer> arrayList = Arrays.asList(arrays);
for (Integer a:arrayList){
System.out.println(a);
}
System.out.println(arrayList);
arrayList.add(6);
System.out.println(arrayList);
}
}
打印结果:
来看Array.asList(array)的源码:
找到了add方法,接着继续往下找
get使用没有问题:
1
2
3
4
5
[1, 2, 3, 4, 5]
4
也不难方向ArrayList<>和List<>导入的包
也有所区别:
ArrayList导入的包:
import java.util.ArrayList;
List导入的包:
import java.util.List;
包的不同也直接导致了底层实现的不同。
3.判断数组中是否包含某个值
方法:contains(target)
public class LearnArray {
public static void main(String[] args) {
Integer[] arrays = {1,2,3,4,5};
// String array = Arrays.toString(arrays);
ArrayList<Integer> arrayList = new ArrayList<Integer>(Arrays.asList(arrays));
// 判断数组是否包含目标元素,包含3
System.out.println(arrayList.contains(3));
// 判断数组是否包含目标元素,不包含6
boolean isContains = Arrays.asList(arrays).contains(6);
System.out.println(isContains);
}
}
打印:
true
false
4.将数组用新分隔符连接
方法:StringUtils.join(List,target)
public class LearnArray {
static Integer[] arrays = {1,2,3,4,5};
static String[] arrays1 = {"a","b","c","d","e"};
public static void testJoin(){
String newArray = StringUtils.join(Arrays.asList(arrays1),"--");
System.out.println(newArray);
System.out.println(Arrays.asList(arrays1));
}
public static void main(String[] args) {
testJoin();
}
}
结果:
a–b--c–d--e
[a, b, c, d, e]
5.二分法查找
方法:Arrays.binarySearch(array,target)
注意
:要查找的target的类型必须和array的类型一致,若查找到则返回target的索引,查找不到则返回负数(貌似查找的数如果比最大的数大的话就返回-(数组大小+1),如果比最小数小就返回-1?)
public static void main(String[] args) {
search();
}
public static void search(){
// 找不到则返回负数
System.out.println(Arrays.binarySearch(arrays,3));
System.out.println(Arrays.binarySearch(arrays,9));
System.out.println(Arrays.binarySearch(arrays,0));
}
结果:
2
-7
-1
6.数组的复制
方法:copyOf(targetArray,int length)
和copyOfRange(targetArray,int from,int to)
static Integer[] arrays = {1,2,3,4,5,6};
public static void cpo(){
Integer[] array = Arrays.copyOf(arrays,8);
System.out.println("复制结果为:"+Arrays.asList(array).toString());
}
public static void main(String[] args) {
//测试copyOf
cpo();
}
结果:
复制结果为:[1, 2, 3, 4, 5, 6, null, null]
注意
:默认从头开始复制,若要复制出来长度大于原数组长度,则多出来的值置为null。
static Integer[] arrays = {1,2,3,4,5,6};
public static void cpof(){
Integer[] array = Arrays.copyOfRange(arrays,1,8);
System.out.println("复制结果为:"+Arrays.asList(array).toString());
}
public static void main(String[] args) {
//测试copyOfRange
cpof();
}
结果:
复制结果为:[2, 3, 4, 5, 6, null, null]
注意
:同样,复制长度超过数组大小时,同样以null补全,不过与copyOf不同的是可以指定复制的起始位置。
7.数组的删除
方法:arrayList.remove(index)
static Integer[] arrays = {1,2,3,4,5,6};
public static void main(String[] args) {
testRemove();
}
public static void testRemove(){
ArrayList<Integer> arrayList = new ArrayList<Integer>(Arrays.asList(arrays));
// 循环下标
int i = 0;
// 通过arrayList的迭代来循环
while (arrayList.iterator().hasNext()){
// 退出条件 索引到达最尾退出
if (i>arrayList.size()-1){
break;
}
// 若该值为3则将改位置删除
if (arrayList.get(i)==3){
arrayList.remove(i);
}
i++;
}
System.out.println(arrayList.toString());
}
打印如下:
[1, 2, 4, 5, 6]
注意注意坑
:因为array在删除一个元素后,会将后边的元素索引往前移动一格,所以如果删除元素后边还是需要被删除的元素,就会往前移动一格,而循环索引又自动+1,故删除不到该元素,我们试着看这段代码👇:
// 注意这里的数组!!
static Integer[] arrays = {2,3,3,3,4};
public static void main(String[] args) {
testRemove();
}
public static void testRemove(){
ArrayList<Integer> arrayList = new ArrayList<Integer>(Arrays.asList(arrays));
// 循环下标
int i = 0;
// 通过arrayList的迭代来循环
while (arrayList.iterator().hasNext()){
// 退出条件 索引到达最尾退出
if (i>arrayList.size()-1){
break;
}
// 若该值为3则将改位置删除
if (arrayList.get(i)==3){
arrayList.remove(i);
}
i++;
}
System.out.println(arrayList.toString());
}
你们猜一下,3会删除干净吗?
。
。
。
。
。
答案是不会,我们看一下运行结果:
[2, 3, 4]
我们再来分析下:
从图中我们可以看到,每次删除一个元素后,该元素后面的元素就会往前移动,而此时循环的 i 在不断地增长,最终会使每次删除 3 的后一个 3 被遗漏,导致删除不掉。
那知道原理就大概知道如何解决了,我们只需要再删除后,将索引向前移一格单位就好了:
打印结果:
[2, 4]
也可以使用迭代器进行删除:
static Integer[] arrays = {2,3,3,3,4};
public static void main(String[] args) {
testRemove2();
}
public static void testRemove2(){
ArrayList<Integer> arrayList = new ArrayList<>(Arrays.asList(arrays));
// 使用迭代器Iterator ite为迭代的每一个元素 ,ite.hasNext()判断是否还有下一个元素,没有则退出循环
for (Iterator<Integer> ite = arrayList.iterator(); ite.hasNext();) {
// 整型需要转换成字符串类型,不然使用不了下边的contains(String)
String str = String.valueOf(ite.next());
if (str.equals("3")) {
ite.remove();
}
}
System.out.println(arrayList);
}
结果如下,成功删除:
[2, 4]
注意
:iterator.remove () 方法在执行的过程中,会把最新的 modCount
赋值给 expectedModCount
,这样在下次循环过程中,modCount 和 expectedModCount 两者就会相等。这两个值若不相等则会报错,在本次程序中,增强for循环做不到使他们两个相等,故需要使用迭代器iterator。
8.将数组转成Set表
其实这个方法和ArrayList差不多,看一下吧:
static String[] arrays2 = {"a","a","a","d","e"};
public static void main(String[] args) {
//测试将数组转成set表
arrayToSet();
}
public static void arrayToSet(){
// 元素内相同元素剔除
Set<String> set = new HashSet<String>(Arrays.asList(arrays2));
System.out.println(set);
}
结果:
[a, d, e]
转成set表会将数组中重复的内容删掉。
9.数组的排序方法
方法:sort(fload array[])
,sort(doule a[],int start,int end)
注意
:数组类型要是数字型的
static Integer[] array3 = {7,2,5,9,1,8};
public static void testSort(){
Arrays.sort(array3);
System.out.println(Arrays.asList(array3).toString());
}
public static void main(String[] args) {
// 测试sort
testSort();
}
结果:
[1, 2, 5, 7, 8, 9]
static Integer[] arrays3 = {7,2,5,9,1,8};
public static void testSort(){
// 复制一份array3
Integer[] copyArray3 = Arrays.copyOf(arrays3,arrays3.length);
Arrays.sort(arrays3);
// 将下标为0到小标为2之间的数排序,注意:左开右闭->[0,2),其实就是交换array[0]-[1]之间的
Arrays.sort(copyArray3,0,2);
System.out.println(Arrays.asList(arrays3).toString());
System.out.println(Arrays.asList(copyArray3).toString());
}
public static void main(String[] args) {
testSort();
}
[1, 2, 5, 7, 8, 9]
[2, 7, 5, 9, 1, 8]
注意我注释的:
2.链表
链表得结构有单向链表
和双向链表
,底层结构是一个双向链表
,如图:
- 链表每个节点我们叫做
Node
,Node 有Prev
属性,代表前一个节点的位置,Next
属性,代表后一个节点的位置;first
是双向链表的头节点,它的前一个节点是 null。last
是双向链表的尾节点,它的后一个节点是 null;- 当链表中没有数据时,first 和 last 是同一个节点,前后指向都是 null;
- 因为是个双向链表,只要机器内存足够强大,是没有大小限制的。
单向链表结构图:
每个结点都有指向下一个结点的指针,每个结点都存放着数据,最后一个结点的下一个结点为Null。
链表的优点
在于,不需要连续的存储单元,修改链表的复杂度为O(1)
(在不考虑查找时);
但是缺点
也很明显:无法直接找到指定节点,只能从头节点一步一步寻找复杂度为O(n)
;
我们 现在先编写一个单链表,并完成链表相关的基本操作,来看看它的流程和基本操作。
1.定义一个单链表结构
Node head = null;
/**
* 定义单链表链表节点
*/
class Node<E> {
//节点包括一个泛型数据内容和指向下一个节点得指针
E data;
Node<E> next = null;
// 构造函数
Node(E data) {
this.data = data;
}
}
2.向链表尾部添加元素
public void addNode(Object value) {
// 实例化一个节点,且该节点结构为 data=value,next=null
Node<Object> newNode = new Node<>(value);
if (head == null) {
//如果头节点为null,说明新插入的节点即为头部节点,将其节点赋给头部
head = newNode;
return;
}
// 否则创建一个临时指针,该指针暂时指向头节点,可以操纵tmp来操作链表,不过head操作不了
Node tmp = head;
// 遍历链表,直到最后一个一个节点,例如 a->b->c->d->null 当遍历到d时,d.next=null,即退出循环
while (tmp.next != null) {
//不断将指针往后移 a->b->c->...
tmp = tmp.next;
}
/*
退出循环后的tmp即为链表最后一个值,然后将最后一个值的next附上新加入的节点
即原先tmp.next=null,现在tmp.next=新节点;然后newNode.next=null,这样就完成了链表尾部的添加
*/
tmp.next = newNode;
}
3.获取链表大小
/***
* 获取链表大小
* @return
*/
public int getLength() {
// 定义初始长度为0的变量
int length = 0;
Node tmp = head;
while (tmp != null) {
length++;
tmp = tmp.next;
}
return length;
}
4.删除指定位置结点
/***
* 删除指定位置结点,索引从1开始
* @param index
* @return
*/
public boolean deleteNode(int index) {
if (index < 1 || index > getLength()) {
return false;
}
if (head==null){
return false;
}
//上边的判断使得该链表现在必定至少有一个结点即为头结点
Node tmp = head;
Node nextTmp = head.next;
// 如果删除的是头部且链表长度为1,则直接将头部赋予空值
if (index == 1 && getLength() == 1) {
head = null;
return true;
}
/* 因为我们是知道有头节点的,如果是删除第一个的话,只需要将头节点向后移动一格
前提是要头节点后面有节点,所以链表大小要大于等于2*/
if (index == 1 /*&& getLength() > 1*/) {
head = head.next;
return true;
}
if (index<=getLength()){
int i = 1;
while (nextTmp!=null){
// 减少一次循环,使得tmp现在的结点为要删除结点(nextTmp)的前一个
if (index==i+1){
break;
}
/* 这里有点绕,就是当下边的nextTmp为最后一个结点时
此时的tmp为倒数第二个结点,应该退出循环,将tmp的下个结点置为空null
然后上边的循环时判断最后一个结点的下一个结点,为空,就退出循环了,必须
要写nextTmp.next!=null不能写nextTmp!=null,可以自己理一下为什么*/
nextTmp=nextTmp.next;
tmp=tmp.next;
i++;
}
// 将要被删除的nextTmp结点的上一个结点指向nextTmp的下一个结点即完成删除
tmp.next = nextTmp.next;
return true;
}
return false;
}
5.打印链表
/***
* 打印链表
*/
public void printList() {
Node tmp = head;
while (tmp != null) {
System.out.print(tmp.data + " ");
tmp = tmp.next;
}
}
6.获取指定位置的数据
/***
* 获取索引为index的值
* @param index
* @return
*/
public Object getData(int index){
if (index<1||index>getLength()){
return false;
}
Node tmp = head;
for (int i=1;i<=index;i++){
if (index==i){
break;
}
tmp=tmp.next;
}
return tmp.data;
}
7.链表进阶操作:
1.反转链表
/***
* 链表反转,传入头部参数
* @param node
*/
public Node reverseList(Node node) {
// 首先定义新链表为空
Node newList = null;
Node next = null;
// 当传入的结点不为空时进行循环
while (node != null) {
//第一遍循环:假如node=1->2->3->4->null,next=1.next=2,所以next=2->3->4->null
/*第二遍循环:node=2->3->4,node.next=2.next=3,所以next=3->4->null*/
next = node.next;
//第一遍循环:node.next=1.next=newLis=null 此时node=1->null
/*第二遍循环:node.next=2.next=newList=1->null,此时node=2->1->null*/
node.next = newList;
//第一遍循环:newList=1->null
/*第二遍循环:newList=2->1->null*/
newList = node;
//第一遍循环:node=2->3->4->null
/*第二遍循环:node=3->4->null*/
node = next;
/***
* 这里分析一下这个循环
* 就是每次循环,先获取到node的下一个结点的链表----1
* 然后让node下一个结点指向新链表newList,因为是要反转链表,
* 所以当前node结点的下一个要是自己前一个结点,而第一遍循环就是将前一个结点赋为null=newList---2
* 接下来将部分反转的node链表赋给newList---3
* 最后再将最开始剔除掉最前边的链表再重新赋给node---4
*
* 总的来说就是每次让node往后移动,将最前边的结点不断指向新链表
* 让新链表从最开始的null,然后被node最前边的结点指完后就变成:
* null<-1
* null<-1<-2
* null<-1<-2<-3
* null<-1<-2<-3<-4
* 然后就得到:4->3->2->1->null
* 打印就会依次打印4 3 2 1了
*/
}
return newList;
}
/***
* 打印链表
*/
public void printList() {
Node node =reverseList(head); //将其反转
Node tmp = node;
while (tmp != null) {
System.out.print(tmp.data + " ");
tmp = tmp.next;
}
}
2.反向打印链表
/***
* 反向打印链表
*/
public void reversePrintList(Node node){
if (node!=null){
//采用递归的方法
reversePrintList(node.next);
System.out.print(node.data + " ");
}
}
3.获取链表中间结点的值
/**
* 查找单链表的中间节点值
*
* @param head
* @return
*/
public Object searchMid(Node head) {
if (head==null){
return null;
}
if (head.next==null){
return head.data;
}
// 定义快慢指针 快的每次循环走两格 慢的每次循环走一格
Node fastPoint = head;
Node slowPoint = head;
/* 当快指针走到头或快指针的下一结点为空,则表明
慢指针走到一半了*/
while (fastPoint!=null&&fastPoint.next!=null){
fastPoint = fastPoint.next.next;
slowPoint = slowPoint.next;
}
return slowPoint.data;
}
4.查找倒数第k个数
/**
* 查找倒数 第k个元素
*
* @param head
* @param k
* @return
*/
public Object findElem(Node head, int k) {
if (k < 1 || k > getLength()) {
return null;
}
//同样是快慢指针
Node fastPoint = head;
Node slowPoint = head;
// 遍历到结尾
for (int i=0;i<getLength();i++){
//快指针先走k步,后面快慢指针一起走
fastPoint=fastPoint.next;
//当当前索引值等于k的时候就让慢指针每次循环移动一格,所以要i>=k
if (i>=k){
slowPoint=slowPoint.next;
}
// 因为fastPoint先走了k步,当fastPoint走到结尾时,慢指针刚好停留在倒数第k个
if (fastPoint==null){
break;
}
}
return slowPoint.data;
}
由于java没有像c语言类似指针的东西,所以理解起和操作起链表总感觉怪怪的,因为当用A=head时,A虽然是引用的head链表,但在java中A已经类似成指针了,操作A也可以对head进行更改…
5.归并排序链表
public class SortLinkList {
static ListNode head = null;
/* 因为java没有指针的概念,一操作head或者head的引用就会改变原始链表
所以要新建一个链表结构存放排好序的链表*/
static ListNode headCp = null;
// 声明链表结构
class ListNode{
Integer val;
ListNode next = null;
ListNode(Integer value){
this.val = value;
}
@Override
public String toString() {
return "ListNode{" +
"val=" + val +
", next=" + next +
'}';
}
}
// 为head链表添加节点
public void addNode(Integer value){
if (head==null){
head = new ListNode(value);
return;
}
ListNode tmp = head;
while (tmp.next!=null){
tmp = tmp.next;
}
tmp.next = new ListNode(value);
return;
}
//打印链表
public void printList(ListNode listNode){
if (listNode==null){
return;
}
while (listNode!=null){
System.out.println(listNode.val);
listNode = listNode.next;
}
return;
}
public static void main(String[] args) {
SortLinkList listNode = new SortLinkList();
// listNode.addNode(5);
listNode.addNode(4);
listNode.addNode(3);
listNode.addNode(2);
listNode.addNode(1);
// listNode.addNode(6);
listNode.sortList(head);
listNode.printList(headCp); // 将排序好的新链表放入打印
}
// 排序
public ListNode sortList(ListNode head) {
if (head==null||head.next==null){
return head;
}
/* 获取mid值,即此时mid为 3->2->1 因为head为4->3->2->1
* 所以你要使用归并排序的话,需要一边左边,一边右边,而操作mid会直接影响到head
* 所以你要获取head的左半边,就要让mid->next=null,这样mid为 3->null
* 此时head为4->3->null*/
ListNode mid = getMid(head);
//上边mid=3->2->1-null,所以right = mid.next = 2->1->null
ListNode right = mid.next;
// System.out.println(mid.toString());
// 使得原始头部head变成原头部的左半边,即4->3->null
mid.next=null;
/* 此时的head为 4->3->null,right为2->1->null
进入归并排序递归,递归是用栈的思想
这里我理解了很久,其实是这样子的:
首先是传入head,递归sortList(),此时head是4->3->null, 而right这边是2->1->null(先别管这边)
然后从上边下来后新的head=4->null,right=3->null,判断head.next=null,返回,进入mergeSort进行第一遍递归。。。。
从递归最深处返回了3->4->null为左边参数,此时到右边参数进行递归,同理。。。
这样到最上层的递归后,左边即为3->4->null,右边为 1->2->null
最后再进行 return mergeSort(3->4->null,1->2->null)的最后归并排序
*/
return mergeSort(sortList(head),sortList(right));
}
public ListNode getMid(ListNode head){
if (head==null||head.next==null){
return head;
}
ListNode slow = head;
ListNode fast = head;
while (fast.next!=null&&fast.next.next!=null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
/**
*
* 归并两个有序的链表
*
* @param head1
* @param head2
* @return
*/
private ListNode mergeSort(ListNode head1, ListNode head2) {
//第一遍递归,传入head1=4->null ,head2=3->null
//最后一遍的排序就是head1=3->4->null,head2=1->2->null
ListNode p1 = head1, p2 = head2;
/*
* 判断head1和head2的头谁大,因为第一次是一个节点,还没形成链
* 让新的链表headCp接管这些节点
*
* 最后一遍,同样是判断开头结点值谁大,因为两边都是从小到大排序的,先比较头让headCp接管最小的
* */
if (head1.val < head2.val) {
headCp = head1;
p1 = p1.next;
} else {
//4>3,让headCp = head2 = 3->null
//最后一遍,此时head1.val=3,head2.val=1,所以让headCp = head2 = 1->2->null
headCp = head2;
// p2往往后移一节 p2 = 3.next = null
//最后一遍,p2 = 1.next = 2->null
p2 = p2.next;
}
//定义headCp的引用,即“指针p”
//最后一遍 p=headCp = 1->->2->null
ListNode p = headCp;
//循环比较链表中的值 因为 第一次p2 = null,所以不进行循环
while (p1 != null && p2 != null) {
//最后一遍,此时p1.val=3,p2.val=2 3>2
if (p1.val <= p2.val) {
p.next = p1;
p1 = p1.next;
p = p.next;
} else {
//最后一遍,进入此判断
//此时p=1->2->null,所以p.next = 1.next = p2 = 2->null,此时p=1->2->null
p.next = p2;
//p2 = 2.next = null 此时p2=null,即将退出循环
p2 = p2.next;
//p = 1.next = 2->null
p = p.next;
}
}
/*下边判断有些只有一个成立*/
//第1条链表空了
if (p1 == null) {
//将 p2 链表后边的所有结点追加到p上,即 追加到headCp上
p.next = p2;
}
//第2条链表空了
//最后一遍,此时p2==null
if (p2 == null) {
//将 p1 链表后边的所有结点追加到p上,即 追加到headCp上
//因为p2为空,而此时p = 3->null,现在让p.next = p1 = 4->null,所以 p = >4->null
/*最后一遍,此时p = 2->null,headCp=1->2->null,p是headCp的引用指针,所以只要排好
p里边2后边指向的结点即可排好headCp中2后边指向的东西
所以p.next=2.next = (p1 = 3->4->null),所以之后 p = 2->3->4->null*/
p.next = p1;
// System.out.println(p.toString());
}
// System.out.println(headCp.toString());
//返回新头 headCp = 3->4->null ,回到上次递归的入口
/*最后一遍 headCp=1->2->null,但因为p=2->3->4->null,而headCp中2之后的结点被p排好了
而p又是headCp的引用,所以headCp=1->2->3->4->null*/
return headCp;
}
}
今天饶了很久才绕过来,先看下结果吧:
结果:
1
2
3
4
5
6
4
5
真的麻烦,java里没有指针的指针的概念,在进行归并排序中,拿到原链表的左右两条链表(按链表中间结点分开),是会将原链表修改成左半边而失去了右半边,所以需要在定义一个链表用来存储排序好的返回,总之好好理解归并排序还是比较重要的,可以练习数组的归并排序,可以先不拿java里的链表来练归并排序,简直就是绕中绕中绕…
当然,java已经提供了创建一个链表的方法,让我们看一下吧:
使用现有LinkList创建链表并进行相关操作
1.创建一个链表:
LinkedList<Integer> linkedList = new LinkedList<>();
2.先看看底层实现了什么方法
上面方法依次是:
getFirst()
:获取链表第一个元素getLast()
:获取最后一个元素removeFirst()
:删除第一个元素并返回删除元素removeLast()
:删除最后一个元素并返回删除元素addFirst(E e)
:在开头前增加新元素addLast(E e)
:在结尾添加新元素contains(Object o)
:查询是否包含某元素size()
:获取链表大小
add(E e)
:向结尾添加新元素remove(int index)
:删除指定位置的元素,索引从0开始addAll(Collection<? extends E> c)
:将传入的所有元素添加到链表结尾addAll(int index, Collection<? extends E> c)
:将传入的所有元素添加到指定位置后clear()
:清空一个链表
依次来看一下方法及打印结果:
■ add()
方法测试:
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(1); //向尾部添加元素
linkedList.add(2);
linkedList.add(3);
linkedList.add(4);
linkedList.add(5);
System.out.println(linkedList);
[1, 2, 3, 4, 5]
■ getFirst()
和getLast()
方法测试
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList.add(4);
linkedList.add(5);
System.out.println(linkedList.getFirst()); //获取第一个元素
System.out.println(linkedList.getLast()); //获取最后一个元素
1
5
■ removeFirst()
和removeLast()
方法测试
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList.add(4);
linkedList.add(5);
System.out.println(linkedList);
linkedList.removeFirst(); //删除第一个
linkedList.removeLast(); //删除最后一个
System.out.println(linkedList); //打印链表
[1, 2, 3, 4, 5]
[2, 3, 4]
■ addFirst()
和addLast()
方法测
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList.add(4);
linkedList.add(5);
System.out.println(linkedList);
linkedList.addFirst(0); //在最前边添加元素0
linkedList.addLast(6); //在最后边添加元素6
System.out.println(linkedList);
[1, 2, 3, 4, 5]
[0, 1, 2, 3, 4, 5, 6]
■ contains()
方法测试
System.out.println(linkedList.contains(5)); //是否包含5
System.out.println(linkedList.contains(15));//是否包含15
true
false
■ size()
方法测
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList.add(4);
linkedList.add(5);
System.out.println(linkedList.size()); //查询链表大小
linkedList.addFirst(0); //在最前边添加元素0
linkedList.addLast(6); //在最后边添加元素6
System.out.println(linkedList.size()); //查询链表大小
5
7
■ remove()
方法测试
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(1);
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList.remove(4);
System.out.println(linkedList);
[1, 1, 1, 2]
■ addAll()
方法测试
LinkedList<Integer> linkedList = new LinkedList<>();
LinkedList<Integer> linkedList2 = new LinkedList<>(); //定义链表2
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList2.add(5);
linkedList2.add(6);
linkedList2.add(7);
System.out.println("合并前:"+linkedList);
linkedList.addAll(linkedList2); //将链表2元素追加到链表1后
System.out.println("合并后:"+linkedList);
合并前:[1, 2, 3]
合并后:[1, 2, 3, 5, 6, 7]
■ addAll(index,Collection)
方法测试
LinkedList<Integer> linkedList = new LinkedList<>();
LinkedList<Integer> linkedList2 = new LinkedList<>(); //定义链表2
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList2.add(5);
linkedList2.add(6);
linkedList2.add(7);
System.out.println("合并前:"+linkedList);
linkedList.addAll(2,linkedList2); //将链表2元素追加到链表1索引为2的地方
System.out.println("合并后:"+linkedList);
合并前:[1, 2, 3]
合并后:[1, 2, 5, 6, 7, 3]
■ clear()
方法测试
LinkedList<Integer> linkedList = new LinkedList<>();
LinkedList<Integer> linkedList2 = new LinkedList<>(); //定义链表2
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList2.add(5);
linkedList2.add(6);
linkedList2.add(7);
System.out.println("合并前:"+linkedList);
linkedList.addAll(2,linkedList2); //将链表2元素追加到链表1索引为2的地方
System.out.println("合并后:"+linkedList);
linkedList.clear(); //清空链表1
System.out.println("清空后:"+linkedList);
合并前:[1, 2, 3]
合并后:[1, 2, 5, 6, 7, 3]
清空后:[]
3.关于删除链表元素的坑
同样的,要使用迭代器Iterator
来进行删除,而不能通过循环获取索引删除,如果要获取索引删除,则在删除后要让索引减1
!
我们来删除一个链表中元素为2的结点👇
错误示范:
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(2);
linkedList.add(2);
linkedList.add(3);
System.out.println("删除前:"+linkedList);
for (int i=0;i<linkedList.size();i++){
if (linkedList.get(i).equals(2)){
linkedList.remove(i);
// i--;
}
}
System.out.println("删除后:"+linkedList);
打印结果
没有删除干净!!!和数组的道理差不多,他会在删除元素后,索引向后移一格,而元素向前移了一格!!如果在删除后将索引向前
移动一格,会发生什么事情呢?
正确示范1:
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(2);
linkedList.add(2);
linkedList.add(3);
System.out.println("删除前:"+linkedList);
for (int i=0;i<linkedList.size();i++){
if (linkedList.get(i).equals(2)){
linkedList.remove(i);
i--; //索引减1
}
}
System.out.println("删除后:"+linkedList);
打印结果:
成功了!
正确示范2:
for (Iterator<Integer> it=linkedList.iterator();it.hasNext();){
String item = String.valueOf(it.next());
if (item.equals("2")){
it.remove();
}
}
打印结果:
通过迭代器也成功删除!!
LinkList底层还实现了队列的接口
。总的来说java中链表和c语言的也有所区别,c语言是有指针的指针,可以指向链表的地址,java当你=head的时候就已经指向同一个地址了,所以可以通过将头赋值给Node,对Node进行迭代而控制head的结构,也是有点绕的,因为我也不大清楚,有种是这样又不是这样的感觉。
3.栈
1.什么是栈?
栈(stack)又名堆栈,它是一种运算受限的
线性表
。限定仅在表尾进行插入和删除
操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底
。向一个栈插入新元素又称作进栈
、入栈
或压栈
,它是把新元素放到栈顶元素的上面
,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈
,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。 ----来源:百度百科
其实栈的结构挺好理解,你只要把栈想象成一口水井
,只有一个出口
,就是上方,假如你要往井里塞东西
,你塞东西进井里就叫入栈
,然后塞满后最上方
的东西就叫栈顶,当然,要是你想拿最下边
的东西,即栈底的东西,你就要将上面的东西一个个拿掉
,拿掉上边的东西就叫做出栈,因为栈的结构也决定了它的特点:先进后出或者叫后进先出。
结构大致图:
我们使用java提供的栈类来测试方法,首先先看下栈的源码👇:
通过源码发现许多方法被加了synchronized修饰,说明这些方法是线程安全
的,然后我们还发现继承了Vector类,我们进入vector类看看👇:
同样的,也是很多被同步锁修饰,所以使用这些方法的时候也是线程安全的
。栈是线程安全的。
2.创建一个栈
创建栈的方法很简单,只需要一行就可以声明一个空栈了:
Stack<Integer> stack = new Stack<>(); //创建一个空栈,用来存储Integer类型的数据
3.栈的相关方法
1.push(),用来将数据压入栈中
public static void main(String[] args) {
testPush();
}
public static void testPush(){
Stack<Integer> stack = new Stack<>();
stack.push(1); //将元素压入栈中
stack.push(2);
stack.push(3);
stack.push(4);
System.out.println(stack);
}
[1, 2, 3, 4]
2.pop(),用来删除栈顶元素,并返回删除的元素
public static void testPop(){
Stack<Integer> stack = new Stack<>();
stack.push(1); //将元素压入栈中
stack.push(2);
stack.push(3);
stack.push(4);
System.out.println("我来打印栈:"+stack);
System.out.println("栈顶被弹出了:"+stack.pop());
System.out.println("我来打印栈:"+stack);
}
我来打印栈:[1, 2, 3, 4]
栈顶被弹出了:4
我来打印栈:[1, 2, 3]
4.peek(),查看此栈顶部的对象,而不将其从堆栈中移除
public static void testPeek(){
Stack<Integer> stack = new Stack<>();
stack.push(1); //将元素压入栈中
stack.push(2);
stack.push(3);
stack.push(4);
System.out.println("我来打印栈:"+stack);
System.out.println("我来查看栈顶,但不删除它:"+stack.peek());
System.out.println("我来打印栈:"+stack);
}
5.isEmpty(),判断栈是否为空
public static void testEmpty(){
Stack<Integer> stack = new Stack<>();
stack.push(1); //将元素压入栈中
stack.push(2);
stack.push(3);
stack.push(4);
System.out.println("查看栈是否为空:"+stack.isEmpty());
stack.clear(); //我来清楚栈的所有元素
System.out.println("查看栈是否为空:"+stack.isEmpty());
}
查看栈是否为空:false
查看栈是否为空:true
5.search(),查找目标元素的索引,注意返回的索引是距离栈顶的距离
public static void testSearch(){
Stack<Integer> stack = new Stack<>();
stack.push(1); //将元素压入栈中
stack.push(2);
stack.push(3);
stack.push(4);
// 返回的索引是相对栈顶的距离!!!找不到为-1
System.out.println("我来找到元素为2的索引:"+stack.search(2));
System.out.println("我来找到元素为1的索引:"+stack.search(1));
System.out.println("我来找到元素为4的索引:"+stack.search(4));
System.out.println("我来找到元素为8的索引:"+stack.search(8));
}
我来找到元素为2的索引:3
我来找到元素为1的索引:4
我来找到元素为4的索引:1
我来找到元素为8的索引:-1
6.stack的遍历
这里就不多说了,就像数组和链表那样,有迭代器删除,也有循环索引减1删除,但这样有点不尊重stack的数据结构
,他这是从前往后遍历的,是直接就拿到栈底元素
了:
public static void testIterator(){
Stack<Integer> stack = new Stack<>();
stack.push(1); //将元素压入栈中
stack.push(2);
stack.push(3);
stack.push(3);
stack.push(3);
stack.push(4);
stack.push(4);
stack.push(4);
stack.push(4);
stack.push(5);
System.out.println("删除前:"+stack);
// 迭代器删除对应元素3
for (Iterator<Integer> it=stack.iterator();it.hasNext();){
String data = String.valueOf(it.next());
if (data.equals("3")){
it.remove();
}
}
//循环删除4
for (int i=0;i<stack.size();i++){
if (stack.get(i)==4){
stack.remove(i);
i--; // 索引向前
}
}
System.out.println("删除后:"+stack);
}
删除前:[1, 2, 3, 3, 3, 4, 4, 4, 4, 5]
删除后:[1, 2, 5]
正确的做法应该是要结合队列一起操作
,这里就提供一下思路,再定义一个栈和队列
,就是当stack不为空时,不断进行stack.pop()弹出栈顶
的循环,并且每次循环先获取stack.peek()的值
,判断是否
是要删除的值,如果不是
将其peek()的值存入队列
中,然后再让队列出队存入新栈
中(队列是先进先出
,这样新栈元素的顺序就和原来的栈的顺序一样了,而且是删除了指定元素的新栈),直到循环结束。
4.队列
1.什么是队列?
队列是一种特殊的
线性表
,特殊之处在于它只允许在表的前端
(front)进行删除
操作,而在表的后端
(rear)进行插入
操作,和栈一样,队列是一种操作受限制的线性表。进行插入
操作的端称为队尾
,进行删除
操作的端称为队头
。 —来源:百度百科
进阶的可以了解一下下边的
在Java多线程应用中,队列的使用率很高,多数
生产消费模型的首选
数据结构就是队列。Java提供的线程安全的Queue可以分为阻塞队列
和非阻塞队列
,其中阻塞队列的典型例子是BlockingQueue
,非阻塞队列的典型例子是ConcurrentLinkedQueue
,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。队列是线程安全的。
队列进阶学习指南:
LinkedBlockingQueue(链表阻塞队列)
SynchronousQueue(锁队列)
DelayQueue(延迟队列)
ArrayBlockingQueue(数组阻塞队列)
队列的结构也挺好理解的,就好比你饭堂打饭
,假设你排队了,就必须打饭
,然后有一条长长的队伍,你在排队,你想走出队伍,就必须等前边的人都走了
你才能走,如果你第一个来打饭,你可以先走,否则就得排队等候,而队伍最前边
是用来走人的,且只能走人,我们称之为队头
,而最后边
只能来人排队,我们称之为队尾
,而这条队伍就称队列。所以也体现了结构特点:即先进先出也叫后进后出。
队列结构大致图:
接下来我们来看一下java实现Queue的源码👇:
可以看到有6个要被实现的方法,而一般的是用LinkList的实现方法👇:
2.创建一条队列
Queue<Integer> queue = new LinkedList<>();
3.队列的相关方法
1.offer(E e),将指定元素添加为此列表的尾部(最后一个元素)
public static void testOffer(){
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
System.out.println(queue);
}
[1, 2, 3, 4]
2.peek(),检索但不删除此列表的头(第一个元素)
public static void testPeek(){
Queue<Integer> queue = new LinkedList<>();
queue.offer(1); // 在队尾添加一个元素
queue.offer(2);
queue.offer(3);
queue.offer(4);
System.out.println(queue);
System.out.println("我是队头:"+queue.peek()); //检索列表第一个元素
System.out.println(queue);
}
[1, 2, 3, 4]
我是队头:1
[1, 2, 3, 4]
3.element(),检索但不删除此列表的头(第一个元素)和栈的peek()差不多
public static void testElement(){
Queue<Integer> queue = new LinkedList<>();
queue.offer(1); // 在队尾添加一个元素
queue.offer(2);
queue.offer(3);
queue.offer(4);
System.out.println(queue);
System.out.println(queue.element());
System.out.println(queue);
}
[1, 2, 3, 4]
1
[1, 2, 3, 4]
这时可能有人会问,那peek()和element()有什么区别
呢?功能都是不删除队头查询队头
,其实区别还是有的,虽然他们功能确实一样,但在处理空队列时会有不同,peek()在处理空队列时返回null,而element()处理空队列时会报错。
先看element()
方法:
果然报错了
再来看看peek()
的:
没有报错
4.poll(),检索并删除此列表的头(第一个元素),和栈的pop()方法差不多
public static void testPoll(){
Queue<Integer> queue = new LinkedList<>();
queue.offer(1); // 在队尾添加一个元素
queue.offer(2);
queue.offer(3);
queue.offer(4);
System.out.println(queue);
System.out.println("我是队头,并且被删除了:"+queue.poll()); //检索列表第一个元素
System.out.println(queue);
}
[1, 2, 3, 4]
我是队头,并且被删除了:1
[2, 3, 4]
5.remove(),检索并删除此列表的头(第一个元素)
public static void testRemove(){
Queue<Integer> queue = new LinkedList<>();
queue.offer(1); // 在队尾添加一个元素
queue.offer(2);
queue.offer(3);
queue.offer(4);
System.out.println(queue);
System.out.println("我是队头,并且被删除了:"+queue.remove()); //检索列表第一个元素
System.out.println(queue);
}
[1, 2, 3, 4]
我是队头,并且被删除了:1
[2, 3, 4]
这时又有人可能会问,remove和poll方法功能不是一样吗,有什么区别吗
?
其实功能确实一样,都是删除队列的开头元素,但区别也还是有的,当要对一个空队列进行删除元素时,remove方法会报错
,而poll方法会返回null
,因此poll方法可以更好的对空列表进行判断。
首先是remove()
方法:
果然报错了!!!
再来看看poll()
方法:
6.队列的遍历
其实队列迭代相比栈相对简单点,因为它的结构,每次弹出队头就好了:
迭代方式1,直接循环弹出队头打印:
public static void testIterator(){
Queue<Integer> queue = new LinkedList<>();
queue.offer(1); // 在队尾添加一个元素
queue.offer(2);
queue.offer(3);
queue.offer(4);
// 判断队列是否为空
while (queue.size()!=0){
//每次循环打印队头,且将队头出队
System.out.println(queue.poll());
}
}
打印:
1
2
3
4
这种迭代其实不太好,因为它把队列全部清空
了,原始的数据虽然打印了出来,但是队列已经为空队列
了,想要保存这个结构就还要需要一个队列
用来保存弹出来的值
,后将其入进新队列
。
迭代方式2,使用新的队列保存数据:
public static void testIterator2(){
Queue<Integer> queue = new LinkedList<>();
//定义一个新队列,用来存放弹出的值
Queue<Integer> queue2 = new LinkedList<>();
queue.offer(1); // 在队尾添加一个元素
queue.offer(2);
queue.offer(3);
queue.offer(4);
while (queue.size()!=0){
System.out.print(queue.peek() + " "); //先不删除,先打印
queue2.offer(queue.poll()); //将弹出来的队头添加到queue2的队尾
}
System.out.println(); //打印换行
System.out.println("我是queue1:"+queue);
System.out.println("我是queue2:"+queue2);
}
1 2 3 4
我是queue1:[]
我是queue2:[1, 2, 3, 4]
也是成功迭代,并且用一个新的列表存放了弹出元素,数据留住了
,但是我们新开辟
了一个队列空间,那还有没有更好的呢?答案是有的,就是使用迭代器Iterator
。
迭代方式3,使用迭代器Iterator:
public static void testIterator3(){
Queue<Integer> queue = new LinkedList<>();
queue.offer(1); // 在队尾添加一个元素
queue.offer(2);
queue.offer(3);
queue.offer(4);
// 使用迭代器进行遍历
for (Iterator<Integer> it=queue.iterator();it.hasNext();){
// 获取当前元素值
String data = String.valueOf(it.next());
System.out.print(data + " ");
}
System.out.println();
System.out.println("我是queue:"+queue);
}
打印
1 2 3 4
我是queue:[1, 2, 3, 4]
成功啦,没有开辟新空间,并且也保存住了数据,还可以在循环里做一些判断,美滋滋。
4.树🌳
1.什么是树?
树状图是一种数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
每个结点有零个或多个子结点;没有父结点的结点称为根结点;每一个非根结点有且只有一个父结点;除了根结点外,每个子结点可以分为多个不相交的子树; —来源:百度百科
2.树的基本术语
- 节点深度:对任意节点x,x节点的深度表示为根节点到x节点的路径长度。所以根节点深度为0,第二层节点深度为1,以此类推
- 节点高度:对任意节点x,叶子节点到x节点的路径长度就是节点x的高度
- 树的深度:一棵树中节点的最大深度就是树的深度,也称为高度
- 父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点
- 子节点:一个节点含有的子树的根节点称为该节点的子节点
- 节点的层次:从根节点开始,根节点为第一层,根的子节点为第二层,以此类推
- 兄弟节点:拥有共同父节点的节点互称为兄弟节点
- 度:节点的子树数目就是节点的度
- 叶子节点:度为零的节点就是叶子节点
- 祖先:对任意节点x,从根节点到节点x的所有节点都是x的祖先(节点x也是自己的祖先)
- 后代:对任意节点x,从节点x到叶子节点的所有节点都是x的后代(节点x也是自己的后代)
- 森林:m颗互不相交的树构成的集合就是森林
- 原文链接:https://blog.youkuaiyun.com/u014532217/article/details/79118023
做了一张图来方便理解:
3.树的类型
1.无序树
树的任意节点的子节点没有顺序关系。
2.有序树
树的任意节点的子节点有顺序关系。
3.二叉树*
树的任意节点至多包含两棵子树。
4.满二叉树
叶子节点都在同一层并且除叶子节点外的所有节点都有两个子节点。
5.完全二叉树
对于一颗二叉树,假设其深度为d(d>1)。除第d层外的所有节点构成满二叉树,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树;也可以这样理解:如果一个二叉树与满二叉树前m个节点的结构相同,这样的二叉树被称为完全二叉树。
(PS:这里的满二叉树和完全二叉树取的是国内的定义,国外的定义不一样,有兴趣的可以去看看国外的定义。)
6.平衡二叉树(AVL)*
它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树,同时,平衡二叉树必定是二叉搜索树。
7.二叉查找树(二叉搜索树、BST)*
若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
任意节点的左、右子树也分别为二叉查找树; 没有键值相等的节点。
8.霍夫曼树(哈夫曼树)
带权路径最短的二叉树称为哈夫曼树或最优二叉树。
9.红黑树-
红黑树是一颗特殊的二叉查找树,除了二叉查找树的要求外,它还具有以下特性:
- 每个节点或者是黑色,或者是红色。
- 根节点是黑色。
- 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NULL)的叶子节点!]
- 如果一个节点是红色的,则它的子节点必须是黑色的。
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
10.B-tree(B-树或者B树)
一颗m阶B树的特性:
根结点至少有两个子女(如果B树只有一个根节点,这个根节点的key的数量可以为[1~m-1]) 每个非根节点所包含的关键字个数 j
满足:⌈m/2⌉ - 1 <= j <= m - 1,节点的值按非降序方式存放,即从左到右依次增加。
除根结点以及叶子节点以外的所有结点的度数正好是关键字总数加1,故内部节点的子树个数 k 满足:⌈m/2⌉ <= k <= m
所有的叶子结点都位于同一层。假定: m:B树的阶 n:非根的内部节点键的个数 t:m阶B树的节点能存在的最小的度 则有:
⌈m/2⌉ - 1 <= n <= m - 1 t - 1 <= n <= 2t -1
11.B+树
m阶B+树是m阶B-tree的变体,它的定义大致跟B-tree一致,不过有以下几点不同:
- 有n棵子树的结点中含有n个关键字,每个关键字不保存数据,只用来索引,所有数据都保存在叶子节点,其中⌈m/2⌉ <= n <= m。
- 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
- 所有的非终端结点可以看成是索引部分,结点中仅含其子树(根结点)中的最大(或最小)关键字。
- 通常在B+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点。
12.B*树
B*树是B+树的变体,除了B+树的要求之外,还有以下特性:
- ⌈m*2/3⌉ <= n <=m 这里的n是除根节点之外的内部节点的键
- 增加内部节点中兄弟节点的指针,由左边指向右边
原文链接:https://blog.youkuaiyun.com/u014532217/article/details/79118023
4.如何定义一个树结构:
//定义一个树结构
public class TreeNode<T>{
//数据项
private T data;
//左结点分支
private TreeNode leftNode;
//右结点分支
private TreeNode rightNode;
//todo 一些方法构造器...
}
■ 算法篇
● 什么是算法?
算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。如果一个算法有缺陷,或不适合于某个问题,执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、空间或效率来完成同样的任务。一个算法的优劣可以用
空间复杂度
与时间复杂度
来衡量。 —来源:百度百科
算法相当于你解题的思维,不过要用机器的思维去思考问题,比如为了得到两个数相加等于二,你可以使用1+1,-1+3,2+0等来得到二,而要选择一个最优的方法得到二,就是你所需要提供的算法。
● 常用的排序算法
- 插入排序
- 希尔排序
- 选择排序
- 堆排序
- 冒泡排序
- 快速排序
- 归并排序
- 基数排序
1.冒泡排序
主要逻辑:在内循环中不断将当前元素和后一个元素比较大小,将大的往后移一格,一直比较到最后,将最大的移动到最后,之后在第二次循环后,就不用比较最后一个元素的值了,所以内循环中的出口是 j < length - i,具体流程如下:
代码:
public class TestBubble {
public static void main(String[] args) {
Integer[] arr = {7, 9, 3, 5, 1, 7, 4, 3, 6, 2, 1, 8, 9, 2, 10};
System.out.println("排序前:" + Arrays.asList(arr));
System.out.println("排序后:" + Arrays.asList(sort(arr)).toString());
}
public static Integer[] sort(Integer[] array) {
int length = array.length - 1;
int temp;
//控制外循环次数
for (int i = 0; i < length; i++) {
boolean disSwap = false;
//控制内循环次数,实际就是比较一前和一后,循环比较,把大的一直往后移
for (int j = 0; j < length - i; j++) {
if (array[j] > array[j + 1]) {
// 如果前面的数大于后面的数则进行交换
temp = array[j] ^ array[j + 1];
array[j] = temp ^ array[j];
array[j + 1] = temp ^ array[j + 1];
disSwap = true;
}
}
//如果现在的数一直和后一个数比,都是后边的数大,则直接返回,不用继续循环
if (disSwap==false){
return array;
}
}
return array;
}
}
排序前:[7, 9, 3, 5, 1, 7, 4, 3, 6, 2, 1, 8, 9, 2, 10]
排序后:[1, 1, 2, 2, 3, 3, 4, 5, 6, 7, 7, 8, 9, 9, 10]
时间复杂度:
- 最好:O(n)
- 最坏:O(n²)
- 平均时间复杂度:O(n²)
2.快速排序
主要逻辑:就是将数组最左边的数,挖一个坑出来,然后定义指向数组前后的指针,从后边开始比较,当后边的数比挖出来的坑的数base小时,则将最前边坑的元素用当前元素替换上,然后将左指针索引向前移一格,当然,前边比较时,就让右边已经挖出来的坑来填上此处坑,最后将坑填上,此时坑左边的数都比base小,坑右边的数都比base大,然后不断递归左边,右边,知道left=right指针递归结束。
可以看图理解一下:
代码
public class TestQuick {
public static void main(String[] args) {
Integer[] arr = {7, 6, 10, 4, 9, 2, 8, 2, 3, 4, 5, 1, 0};
System.out.println(Arrays.asList(sort(arr, 0, arr.length - 1)).toString());
}
public static Integer[] sort(Integer[] array, int start, int end) {
int length = array.length - 1;
if (start < end && end <= length) {
int left = start;
int right = end;
// 挖坑 将第一个元素挖出来 准备将其填入其它坑
int base = array[left];
while (left < right) {
while (array[right] >= base && left < right) {
right--;
}
/*上边循环退出条件为找到的array[right]比base小 或者 left=right
* 此时让当前array[right]的值赋给已经被挖坑的array[left],后让left索引后移
* */
if (left < right) {
array[left] = array[right];
left++;
}
while (array[left] <= base && left < right) {
left++;
}
/*循环退出条件为找到的array[left]比base大 或者 right=left,原理和上边差不多,只是反了过来
* 因为上边的array[right]被挖空了,所以现在将array[left]比base大的放入坑中,这样左边
* 的数都比base小,右边都比base大
* */
if (left < right) {
//挖走array[left]的元素给[right],此时[left]此处便为坑,后面将它填上就完成了
array[right] = array[left];
right--;
}
//最后填坑,将最初的base填入坑中,至此填完后base左边都比base右边小
array[left] = base;
//递归基准base的左右两边,此时的基准为left,left左边都比base小,右边都比base大
sort(array, start, left);
sort(array, left + 1, end);
}
}
return array;
}
}
排序前:[7, 6, 10, 4, 9, 2, 8, 2, 3, 4, 5, 1, 0]
排序后:[0, 1, 2, 2, 3, 4, 4, 5, 6, 7, 8, 9, 10]
时间复杂度:
- 最好:O(nlogn)
- 最坏:O(n²)
- 平均时间复杂度:O(nlogn)
3.归并排序
引用了这篇的文字和图片,有兴趣的同学可以看看:网页链接
分而治之(divide - conquer);每个递归过程涉及三个步骤
- 第一, 分解: 把待排序的 n 个元素的序列分解成两个子序列, 每个子序列包括 n/2 个元素。
- 第二, 治理: 对每个子序列分别调用归并排序MergeSort, 进行递归操作。
- 第三, 合并: 合并两个排好序的子序列,生成排序结果。
代码:
public class TestMerge {
public static void main(String[] args) {
Integer[] arr = {7,6,2,1,9,8,4,3};
System.out.println(Arrays.asList(splitArray(arr,0,arr.length-1)).toString());
}
public static Integer[] splitArray(Integer[] array, int left, int right) {
int length = array.length - 1;
if (left < right && right <= length && left >= 0) {
int mid = (left + right) / 2;
//不断递归取出数组一半前的最前边两个
splitArray(array, left, mid);
//递归取出数组一半后的最前边两个
splitArray(array, mid + 1, right);
//将一个个排好序的小数组合成大数组
return mergeSort(array, left, mid, right);
}
return array;
}
public static Integer[] mergeSort(Integer[] array, int left, int mid, int right) {
//不要用array.length-1 因为可能会需要局部归并,创建大小为right-left+1的数组,实际索引到right-left
Integer[] temp = new Integer[right - left +1];
int l = left;
int m = mid + 1;
int k = 0; // 控制下标
while (l <= mid && m <= right) {
if (array[l] < array[m]) {
temp[k] = array[l]; //让首项为小的
//索引往后
l++;
} else {
temp[k] = array[m];
m++;
}
k++; //索引往后
}
//下边两个循环只会执行一个
//说明右边已经遍历完了,将剩下的左边补给temp
while (l <=mid) {
temp[k] = array[l];
l++;
k++;
}
//说明左边已经遍历完了,将剩下的右边补给temp
while(m<=right) {
temp[k] = array[m];
k++;
m++;
}
//将排号的temp数组存入array返回给外面一层递归使用
for (int i = 0; i < temp.length; i++) {
array[i+left] = temp[i]; //i+left是因为需要根据递归进来一层一层往上边修改对应的索引值
}
return array;
}
}
排序前:[7, 6, 2, 1, 9, 8, 4, 3]
排序后:[1, 2, 3, 4, 6, 7, 8, 9]
时间复杂度:
最好:O(nlogn)
最坏:O(nlogn)
平均时间复杂度:O(nlogn)
这里不推荐使用冒泡,毕竟时间复杂度比较高,而归并排序和冒泡排序视情况而定,因为归并相对快排更稳定
一些,当数据量非常大
时推荐使用归并排序。
补充问题:
1.ArrayList 和 LinkedList 有何不同?
答:可以先从底层数据结构开始说起,然后以某一个方法为突破口深入,比如:最大的不同是两者
底层
的数据结构不同,ArrayList
底层是数组
,LinkedList 底层是双向链表
,两者的数据结构不同也导致了操作的 API 实现有所差异,拿新增实现来说,ArrayList
会先计算并决定是否扩容
,然后把新增的数据直接赋值到数组上,而 LinkedList 仅仅只需要改变插入节点和其前后节点的指向位置
关系即可。
2.ArrayList 和 LinedList 是线程安全的么,为什么?
答:当两者作为
非共享变量
时,比如说仅仅是在方法里面的局部变量时,是没有线程安全问题
的,只有当两者是共享变量
时,才会有线程安全问题
。主要的问题点在于多线程
环境下,所有线程任何时刻都可对数组和链表进行操作,这会导致值被覆盖,甚至混乱的情况。
如果有线程安全问题,在迭代的过程中,会频繁报ConcurrentModificationException
的错误,意思是在我当前循环的过程中,数组或链表的结构被其它线程修改了。
3. 如何解决线程安全问题?
答:Java 源码中推荐使用
Collections#synchronizedList
进行解决,Collections#synchronizedList 的返回值是 List 的每个方法都加了synchronized
锁,保证了在同一时刻,数组和链表只会被一个线程所修改,或者采用CopyOnWriteArrayList 并发 List
来解决。
4. ArrayList 和 LinkedList 应用场景有何不同
答:
ArrayList
更适合于快速的查找匹配
,不适合频繁新增删除,像工作中经常会对元素进行匹配查询的场景比较合适,LinkedList
更适合于经常新增和删除
,对查询反而很少的场景。
5.哪些队列具有阻塞的功能,大概是如何阻塞的?
答:队列主要提供了两种阻塞功能,如下:
LinkedBlockingQueue
链表阻塞队列和ArrayBlockingQueue
数组阻塞队列是一类,前者容量是 Integer 的最大值,后者数组大小固定,两个阻塞队列都可以指定容量大小,当队列满时,如果有线程 put 数据,线程会阻塞住,直到有其他线程进行消费数据后,才会唤醒阻塞线程继续 put,当队列空时,如果有线程take数据,线程会阻塞到队列不空时,继续 take。
SynchronousQueue
同步队列,当线程 put 时,必须有对应线程把数据消费掉,put 线程才能返回,当线程 take 时,需要有对应线程进行 put 数据时,take 才能返回,反之则阻塞,举个例子,线程 A put 数据 A1 到队列中了,此时并没有任何的消费者,线程 A 就无法返回,会阻塞住,直到有线程消费掉数据 A1 时,线程 A 才能返回。
6.底层是如何实现阻塞的?
答:队列本身并没有实现阻塞的功能,而是利用
Condition
的等待唤醒机制,阻塞底层实现就是更改线程的状态为沉睡。
打个比方,当队列满时,使用 put 方法,会一直阻塞到队列不满为止。当队列空时,使用 take 方法,会一直阻塞到队列有数据为止。
■ 总结
未完待续…