0%

Chp6二分搜索树(Binary Search Tree)

一、树结构

使用树结构的原因:

1.树结构是一种天然的组织结构

img

2.数据使用树结构存储后,查找高效

二叉树:是动态数据结构【不需要在创建数据结构的时候就决定好要存储多少数据,要添加元素就 new 一个新的空间加入到其中即可,删除二、基础概念同理】

节点 Node 中,除了要存放元素 e 之外,还要存储两个指向其他节点的引用 left【左孩子】 和 right 【右孩子】

二叉树要点:

1.二叉树具有唯一的根节点;

2.二叉树中每个节点最多有两个孩子,每个节点最多有一个父亲节点;一个孩子也没有的叫做叶子节点

3.二叉树具有天然的递归结构【每个节点的左子树也是二叉树、每个节点的右子树也是二叉树】

img

4.二叉树不一定都是“满”的【满二叉树:除叶子节点外,每个节点都有两个孩子】

5.一个节点、甚至 NULL 也是二叉树

二、构建二分搜索树:

二分搜索树的性质:

1.二分搜索树是二叉树

2.二分搜索树的每个节点的值:大于其左子树的所有节点的值且小于其右子树的所有节点的值

3.二分搜索树的每个子树也是二分搜索树

4.存储的元素必须具有可比较性【存储自定义数据类型,必须自定义好数据的比较方式

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
public class BST<Key extends Comparable<Key>,Value> {		//E 要满足Comparable接口,要有可比较性
private class Node { //声明对应的节点类型
private Key key;
private Value value;
private Node left, right;

public Node(Key key,Value value) {
this.key = key; //e为用户传来的e
this.value = value; //左孩子初始化
left = right = null; //右孩子初始化
}
}

private Node root; //根节点root
private int count; //count 记录当前二叉树存储元素的数量

public BST(){ //二分搜索树的构造函数
root = null;
count = 0;
}
//返回二分搜索树的节点个数
public int size(){
return count;
}
//返回二分搜索树是否为空
public boolean isEmpty(){ //二分搜索树为空
return count == 0;
}
}

三、向二分搜索树中添加元素

图解步骤

1,要插入元素60,先从根root节点进行比较,由于60比41大,所以要把60插入到41的右子树。

img

2,而41的右子树存在元素,因此61不能直接插入。于是60与41的右子树58进行比较,由于60比58大,因此要插入到58的右子树

img

3,而58的右子树没有元素,则直接将60插入58的右子树。

img

img特殊情况(有重复的话,该元素就相当于已经存在于树中,不做任何改变)

如果想要包含重复元素,只需定义:左子树 <= 节点;右子树 >= 节点【只需将 = 关系放到定义中即可】

代码实现:

insert–在以node为根的二分搜索树中插入节点(key,value)

使用递归算法

递归出口:1,当前节点为空;2,插入的key比node节点中的key小,且左子树为空;3、插入的key比Node节点中的key大且右子树为空;4、

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
// 向二分搜索树中添加新的元素e
public void insert(Key key,Value value){

if(root == null){
root = new Node(e); //根节点直接指向新建的元素即可
count ++;
}
else
insert(root, key ,value); //从根节点添加新元素e
}

// 向以node为根的二分搜索树中插入元素e,递归算法
private void insert(Node node, Key key ,Value value){ //传入参数 Node 与 e
if(key.compareTo(node.key)==0) //【递归的终止条件1】传入的key已经存在于树中
node.value = value;
return;
else if(key.compareTo(node.key) < 0 && node.left == null){
//【递归的终止条件2】插入的key比node节点中的key小,且左子树为空
node.left = new Node(key,value); //插入左子树中,直接让node的左孩子为新插入的e
count ++;
return;
}
else if(key.compareTo(node.key) > 0 && node.right == null){
//【递归的终止条件3】插入的key比Node节点中的key大且右子树为空
node.right = new Node(key,value);//插入右子树中,直接让node的右孩子为新插入的e
count ++;
return;
}

if(key.compareTo(node.key) < 0) //递归调用,插入的e比Node节点中的e小,e插入左子树
insert(node.left, key,value);
else //e.compareTo(node.e) > 0
insert(node.right, key,value); //node 的右孩子成为元素e
}

精简代码:递归出口改为一个:即,只要node为null则必须插入新节点。将节点与二叉树挂起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 向以node为根的二分搜索树中插入元素e,递归算法
// 返回插入新节点后二分搜索树的根
private Node insert(Node node,Key key,Value value){
if(node == null){ //只要node 为null,则必须新插入节点
count ++;
return new Node(key,value); //将节点与二叉树挂接起来
}
if(key.compareTo(node.key)==0)
node.value = value;
else if(e.compareTo(node.e) < 0)
node.left = insert(node.left, e); //传入的key小于当前节点,则用递归函数将其插入到node的左子树中
else // key.compareTo(node.key) > 0
node.right = insert(node.right, e);
return node; //最后把以node为根的树返回回去
}
}

四、 二分搜索树的查询操作

contain–查看二分搜索树是否存在键key

使用递归算法

递归出口:1,传入的node为null,返回false;2,传入的key与node的key相等则找到,返回true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 看二分搜索树中是否包含元素e(新增代码)
public boolean contain(Key key){
return contain(root,key); //从根节点开始查看
}

// 查看以node为根的二分搜索树中是否包含键值为key的节点, 使用递归算法
private boolean contain(Node node,Key key){

if(node == null) //[递归出口1]如果传入的node为空直接返回false
return false;
if(key.compareTo(node.key) == 0) //[递归出口2]如果传入的key与node的key相等则找到,返回true
return ture;
else if(key.compareTo(node.key) < key) //如果传入的key小于node的key,则向递归函数中传入node的左子节点,进行递归调用
return contain(node.left,key);
else //key.compareTo(node.key)>0
return contain(node.right,key);
}

五、search–二分搜索树中搜索键key所对应的值value。

使用

递归出口:1,当前节点为空,返回null;2,传入的key与当前节点node的key相等,则直接返回node.value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Value search(key key){
return search(root,key);//从根节点开始搜索
}
// 在以node为根的二分搜索树中查找key所对应的value, 递归算法
// 若value不存在, 则返回NULL
private Value search(Node node,Key key){

if(node == null) //[递归出口1]如果传入的node为空直接返回false
return null;
if(key.compareTo(node.key)==0) //[递归出口2]如果传入的key与node的key相等则找到,返回node.value
return node.value;
else if(key.compareTo(node.key)<0) //如果传入的key小于node的key,则向递归函数中传入node的左子节点,进行递归调用
return search(node.left,key);
else //key.compareTo(node.key)>0
return search(node.right,key);
}

六、遍历二叉搜索树

img

前序遍历

只要node非null。首先打印当前node自己的key值;再通过递归函数遍历node的左子节点;最后通过递归函数遍历node的右子节点。

1
2
3
4
5
6
7
8
9
10
11
public void preOrder(){
preorder(root); //从根节点开始
}
// 对以node为根的二叉搜索树进行前序遍历, 递归算法
private void preOrder(Node node){
if(node !=null){ //在node不为空的情况下,则进行遍历
System.out.println(node.key);
preOrder(node.left);
preOrder(node.right);
}
}

中序遍历(遍历后是从小到大排好序的)

只要node非null。首先用递归函数遍历node的左子节点;再打印当前node的key值;最后用递归函数遍历node的右子节点。

1
2
3
4
5
6
7
8
9
10
public void inOrder(){
inOrder(root);
}
private void inOrder(Node node){
if(node != null){
inOrder(node.left);
System.out.println(node.key);
inOrder(node.right);
}
}

后续遍历

只要node非null。首先用递归函数遍历node的左子节点;再用递归函数遍历node的右子节点;最后打印当前node的key值。

1
2
3
4
5
6
7
8
public void postOrder(){
postOrder();
}
private void postOrder(Node node){
postOrder(node.left);
postOrder(node.right);
System.out.println(node.key);
}

七、二分搜索树的层序遍历

img

思路:需要借助队列实现。先把根节点root入队。进行循环条件是队列不为空,队首元素出列,打印该节点node的key值,随后该节点的左子节点和右子节点入队。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void levelOrder(){
//我们使用LinkedList来作为我们的队列
ListedList<Node> q = new LinkedList<Node>();
q.add(root);
while(! q.isEmpty()){

Node node = q.remove();//当前节点出队
System.out.println(node.key); //打印当前节点的key

if(node.left != null)
q.add(node.left);
if(node.right != null)
q.add(node.right);
}
}

八、寻找最小和最大的key的节点

思路:定义递归函数。递归出口:若该节点左(右)子节点为空,则直接返回其自身;其左(右)子节点不为空,则递归调用其左(右)子节点

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
// 寻找二分搜索树的最小的键值
public Key minimum(){
assert count != 0; //只要二分搜索树不为空
Node minNode = minimum( root );
return minNode.key;

}
// 返回以node为根的二分搜索树的最小键值所在的节点
private Node minimum(Node node){
if( node.left == null ) //递归出口
return node; //若该节点左子节点为空,则直接返回其自身

return minimum(node.left); //其左子节点不为空,则递归调用其左子节点
}


// 寻找二分搜索树的最大的键值
public Key maximum(){
assert count != 0;
Node maxNode = maximum(root);
return maxNode.key;
}

// 返回以node为根的二分搜索树的最大键值所在的节点
private Node maximum(Node node){
if( node.right == null )
return node;

return maximum(node.right);
}

九、删除最小和最大key的节点

思路: 定义递归函数。递归出口:当前节点node的左孩子为空(即证明当前节点为最小节点),用当前节点的右子节点node.right,代替当前节点返回给当前节点的母节点。

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
// 从二分搜索树中删除最小值所在节点
public void removeMin(){
if( root != null )
root = removeMin( root );
}

// 从二分搜索树中删除最大值所在节点
public void removeMax(){
if( root != null )
root = removeMax( root );
}
private Node removeMin(Node node){

if( node.left == null ){ //递归出口。如果当前节点的左孩子为空则当前节点就是最小节点

Node rightNode = node.right; //把node节点的右孩子,作为原来node节点的父节点的左孩子
node.right = null;
count --;
return rightNode; //用当前node的右孩子代替了node
}

node.left = removeMin(node.left); //当前节点左孩子不为空,则递归调用。
return node;
}

// 删除掉以node为根的二分搜索树中的最大节点
// 返回删除节点后新的二分搜索树的根
private Node removeMax(Node node){

if( node.right == null ){

Node leftNode = node.left;
node.left = null;
count --;
return leftNode;
}

node.right = removeMax(node.right);
return node;
}

十、删除二分搜索树中的任意节点

img

img

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
// 从二分搜索树中删除键值为key的节点
public void remove(Key key){
root = remove(root, key);
}
// 删除掉以node为根的二分搜索树中的最小节点
// 返回删除节点后新的二分搜索树的根
private Node remove(Node node, Key key){

if( node == null )
return null;

if( key.compareTo(node.key) < 0 ){
node.left = remove( node.left , key );
return node;
}
else if( key.compareTo(node.key) > 0 ){
node.right = remove( node.right, key );
return node;
}
else{ // key == node->key

// 待删除节点左子树为空的情况
if( node.left == null ){
Node rightNode = node.right;
node.right = null;
count --;
return rightNode;
}

// 待删除节点右子树为空的情况
if( node.right == null ){
Node leftNode = node.left;
node.left = null;
count--;
return leftNode;
}

// 待删除节点左右子树均不为空的情况

// 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
// 用这个节点顶替待删除节点的位置
Node successor = new Node(minimum(node.right));
count ++;

successor.right = removeMin(node.right);
successor.left = node.left;

node.left = node.right = null;
count --;

return successor;
}
}

十一、测试二分搜索树

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
// 测试二分搜索树
public static void main(String[] args) {

int N = 1000000;

// 创建一个数组,包含[0...N)的所有元素
Integer[] arr = new Integer[N];
for(int i = 0 ; i < N ; i ++)
arr[i] = new Integer(i);

// 打乱数组顺序
for(int i = 0 ; i < N ; i ++){
int pos = (int) (Math.random() * (i+1));
Integer t = arr[pos];
arr[pos] = arr[i];
arr[i] = t;
}

// 我们测试用的的二分搜索树的键类型为Integer,值类型为String
// 键值的对应关系为每个整型对应代表这个整型的字符串
BST<Integer,String> bst = new BST<Integer,String>();
for(int i = 0 ; i < N ; i ++)
bst.insert(new Integer(arr[i]), Integer.toString(arr[i]));

// 对[0...2*N)的所有整型测试在二分搜索树中查找
// 若i在[0...N)之间,则能查找到整型所对应的字符串
// 若i在[N...2*N)之间,则结果为null
for(int i = 0 ; i < 2*N ; i ++){
String res = bst.search(new Integer(i));
if( i < N )
assert res.equals(Integer.toString(i));
else
assert res == null;
}
}

注意事项:

由于我们实现的二分搜索树不是平衡二叉树,所以如果按照顺序插入一组数据,我们的二分搜索树会退化成为一个链表。

Chp5链表和递归

LeetCode中链表的相关问题

删除链表中等于给定值val的所有元素。

示例

给定:1–>2–>6–>3–>4–>5–>6 ,val =6

返回:1–>2–>3–>4–>5

解题思路:调用在LeetCode中定义好的LinkedList类

1,首先要求头结点不是空的,如果头节点中包含val,则直接删除,但是要用循环,因为head.next和之后的Node中也许会还包含指定元素

2,如果链表中从开始到最后都是要删除的节点,则直接返回空。

3,找到待删除元素的前一个节点。因为此时的,head肯定不为空,所以把prev设为head。

4,只要该节点(prev)的下一个节点(prev.next)不为空,如果有包含val的节点。则删除该节点;否则prev向后移动一位。

5,结束所有的操作后最后返回head节点。

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {//传入头结点和要删除元素所包含的节点。
while(head != null && head.val == val){
ListNode delNode = head; //定义待删除的节点
head = head.next; //绕过我们要删除的节点
delNode.next = null;
}
if(head == null)
return null; //也许该链表中所有节点都是要被删除的节点

ListNode prev = head; //找到待删除元素前一个节点
while(prev.next != null){
if(prev.next.val == val){ //删除prev.next但是prev不用向后挪。
ListNode delNode = prev.next;
prev.next = delNode.next;
delNode = null;
}
else
prev = prev.next; //prev向后挪一位
}

return head;
}
}

Chp4链表 LinkedList

一、基本概念

链表:最简单的动态数据结构

重要性:更深入的理解引用和递归,辅助组成其他数据结构

优点:真正的动态,不需要处理固定的容量

缺点:丧失了随机访问的能力

与数组对比:数组最好用于索引有语义的情况(例 score[2]),其最大的有点是支持快速查询;

img

特点:

图解:数据存储在 “节点”(Node)中,实际存储在 e 中,数据与数据之间的链接(对下一个链表的引用)由 next 完成;最后一个节点 next 存储的为 Null;如果为Null则证明是最后一个节点。

img

首先在链表类中定义私有内部类Node

而Node中next的定义类型也称作“自引用”式,因为Node中包含了一个和自己类型相同的字段(本例中为next)。类型Node的next字段仅仅是对另外的一个Node对象的“引用”,而不是一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private class Node{         //用户不需要知道底层是如何实现的,因此设置为私有类。
public E e; //data
public Node next; //指向下一个Node的引用。自引用。

public Node(E e,Node next){ //声明构造函数,用户也同时传来 e 和 next
this.e = e; //声明构造函数,用户也同时传来 e 和 next
this.next = next; //将当前节点的next赋值成用户传来的next
}
public Node(E e){ //用户只传来e
this(e,null); //将当前节点的e赋值成用户传来的e,next设为null
}
public Node(){ //用户什么都不传
this(null,null); //将e和next都设为null
}
@Override
public String toString(){ //重写toString方法
e.toString();
}
}

二、 在链表中添加元素

对于一个链表来说,想要访问存储在其中的所有节点,我们必须把链表的头 head 存储起来,

img

在链表头添加元素:

图解(要将 666【叫做node】 加入链表中而不破坏现有的链表结构)

img

步骤:1.让 node 的 next 指向现在链表的头,即执行 node.next = head

img

  1. 此时,666 成为新的链表头,让 head 指向新的 666 节点,即执行 head = node

    img

在链表中间添加元素:

图解:将 666 插入到链表中 1 的位置

img

步骤:1.要插入节点 666 ,必须要找到插入666之后,这个节点之前的节点是谁,将其叫做 prev,其初始化和 head 在同一个地方,执行 Node prev = head;

img

步骤2:要找到 666 之前节点 ,因为插入 666 的索引为2【链表中并没有索引这个概念】故之前节点的索引为 1 ,从0开始遍历,遍历到索引为1的位置即可;

img

步骤3:将 node 的 next 指向 prev 的下一个元素,即执行 node.next = prev.next

img

步骤4:prev 的 next 指向 node, 即执行 node.next = prev.next,成功将 666 插入到链表中

img

关键:找到要添加的节点的前一个节点(prev)

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
public class LinkedList<E> {

private class Node{
public E e;
public Node next;

public Node(E e, Node next){
this.e = e;
this.next = next;
}

public Node(E e){
this(e, null);
}

public Node(){
this(null, null);
}

@Override
public String toString(){
return e.toString();
}
}
//(新增代码)
private Node head; //声明Node型变量head
private int size; //size记录链表中有多少个元素

public LinkedList(){ //构造函数,链表初始化时的head与size
head = null;
size = 0;
}

// 获取链表中的元素个数(新增代码)
public int getSize(){
return size;
}

// 返回链表是否为空(新增代码)
public boolean isEmpty(){
return size == 0;
}

// 在链表头添加新的元素e(新增代码)
public void addFirst(E e){
// Node node = new Node(e); //创建新的节点 e
// node.next = head; //让 node 的 next 指向现在链表的头
// head = node; //让 head 指向新的节点

head = new Node(e, head); //上面三行代码的优雅写法
size ++;
}

// 在链表的index(0-based)位置添加新的元素e(新增代码)
// 在链表中不是一个常用的操作,练习用:)
public void add(int index, E e){

if(index < 0 || index > size) //判断 index 的合法性
throw new IllegalArgumentException("Add failed. Illegal index.");

if(index == 0)
addFirst(e);
else{
Node prev = head;
for(int i = 0 ; i < index - 1 ; i ++)
prev = prev.next;
//把当前 prev 存的下一个节点放到 prev 变量中,prev会在链表中一直移动,直到 index - 1 这个位置

// Node node = new Node(e); //创建node,元素为 e
// node.next = prev.next; //将 node 的 next 指向 prev 的下一个元素
// prev.next = node; //prev 的 next 指向 node

prev.next = new Node(e, prev.next); //上面三行的优雅写法
size ++;
}
}

// 在链表末尾添加新的元素e(新增代码)
public void addLast(E e){
add(size, e);
}
}

三、使用链表的虚拟头结点

上面所述的方法中在链表头添加元素时存在特殊:为链表添加元素的过程要找到待添加位之前的节点,但对于链表头来说并没有之前的节点,所以在逻辑上特殊。通过使用链表的虚拟头节点即可将链表头添加元素与其他位置添加元素统一起来。

图解:创建个虚拟节点即可解决链表头添加元素时的问题,对链表来说,第一个元素是 dummyHead 的 next 对应的节点的元素,而不是 dummyHead 对应的节点的元素,dummyHead 这个位置的元素是根本不存在的,对用户来说也没有意义,只是为了逻辑编写方便设置的虚拟头结点。这样所有的元素都有前一个位置的节点,并且初始的时候 dummyHead 指向的就是 0 这个元素的前一个节点。

img

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
public class LinkedList<E> {

private class Node{
public E e;
public Node next;

public Node(E e, Node next){
this.e = e;
this.next = next;
}

public Node(E e){
this(e, null);
}

public Node(){
this(null, null);
}

@Override
public String toString(){
return e.toString();
}
}

private Node dummyHead; //引入虚拟头结点(修改代码)
private int size;

public LinkedList(){
dummyHead = new Node();//(修改代码)
size = 0;
}

// 获取链表中的元素个数
public int getSize(){
return size;
}

// 返回链表是否为空
public boolean isEmpty(){
return size == 0;
}

// 在链表的index(0-based)位置添加新的元素e
// 在链表中不是一个常用的操作,练习用:)
public void add(int index, E e){

if(index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Illegal index.");

Node prev = dummyHead; //
for(int i = 0 ; i < index ; i ++) //找到 index 位置的前一个节点
prev = prev.next; //prev 初始时是从 dummyHead 开始的(修改代码)

prev.next = new Node(e, prev.next);
size ++;
}

// 在链表头添加新的元素e
public void addFirst(E e){
add(0, e); //(修改代码)直接复用 add 即可
}

// 在链表末尾添加新的元素e
public void addLast(E e){
add(size, e);
}
}

四、链表遍历,查询和修改

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
 // 获得链表的第index(0-based)个位置的元素
// 在链表中不是一个常用的操作,练习用:)
public E get(int index){

if(index < 0 || index >= size)
throw new IllegalArgumentException("Get failed. Illegal index.");

Node cur = dummyHead.next;
for(int i = 0 ; i < index ; i ++)
cur = cur.next;
return cur.e;
}

// 获得链表的第一个元素
public E getFirst(){
return get(0);
}

// 获得链表的最后一个元素
public E getLast(){
return get(size - 1);
}

// 修改链表的第index(0-based)个位置的元素为e
// 在链表中不是一个常用的操作,练习用:)
public void set(int index, E e){
if(index < 0 || index >= size)
throw new IllegalArgumentException("Set failed. Illegal index.");

Node cur = dummyHead.next;
for(int i = 0 ; i < index ; i ++)
cur = cur.next;
cur.e = e;
}

// 查找链表中是否有元素e
public boolean contains(E e){
Node cur = dummyHead.next;
while(cur != null){
if(cur.e.equals(e))
return true;
cur = cur.next;
}
return false;
}

@Override
public String toString(){
StringBuilder res = new StringBuilder();

// Node cur = dummyHead.next;
// while(cur != null){
// res.append(cur + "->");
// cur = cur.next;
// }
for(Node cur = dummyHead.next ; cur != null ; cur = cur.next)
res.append(cur + "->");
res.append("NULL");

return res.toString();
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {

public static void main(String[] args) {

LinkedList<Integer> linkedList = new LinkedList<>();
for(int i = 0 ; i < 5 ; i ++){
linkedList.addFirst(i);
System.out.println(linkedList);
}

linkedList.add(2, 666);
System.out.println(linkedList);
}
}

五、删除元素

删除元素和添加元素基本类似,都是定位之前一个删除元素之前的元素 修改元素指向才是改变链表的本质(修改元素并不能改变链表)

img

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
public E remove(int index){
if(index < 0 || index >= size)
throw new IllegalArgumentException("Remove failed. Index is illegal.");

Node prev = dummyHead;
for(int i = 0 ; i < index ; i ++)
prev = prev.next;

Node retNode = prev.next;
prev.next = retNode.next;
retNode.next = null;
size --;

return retNode.e;
}

// 从链表中删除第一个元素, 返回删除的元素
public E removeFirst(){
return remove(0);
}

// 从链表中删除最后一个元素, 返回删除的元素
public E removeLast(){
return remove(size - 1);
}

// 从链表中删除元素e
public void removeElement(E e){

Node prev = dummyHead;
while(prev.next != null){
if(prev.next.e.equals(e))
break;
prev = prev.next;
}

if(prev.next != null){
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
size --;
}
}

六、链表中的方法和对应的时间复杂度

img

Chp3栈和队列

栈 Stack

概念:

栈是一种线性结构;

相比数组,栈对应操作的是数组的子集;

只能从一端添加元素,也只能从一段取出元素;

这一段称作栈顶。

性质:

栈是一种先进后出的数据结构;Last In First Out;在计算机的世界里栈有着不可思议的作用。

栈的应用:

Undo操作(撤销)

栈的实现

Stack

构造方法:根据参数规定的容量创建一个新栈,栈的域包括表示最大容量的变量(即数组的大小)。

push()方法:将top(栈顶)的值增加一,使它指向原顶端数据项上面的一个位置,并在这个位置上存储一个数据项。

pop()方法: 从栈中移除数据项。返回index为top(栈顶)的数据项值,然后top减一。

peek()方法:返回栈顶(top)元素所指的数据项的值,不对栈做任何变动。

LeetCode例题

给定一个只包括

思路

1,import java.util.Stack。并声明一个栈

2,遍历这个字符串,每次用charAt()具体来看字符串中第i个字符是什么样子的,存成 char c

3,如果c==’(‘ || ‘[‘ ||’{‘ 满足这个条件,将c push进入栈中;否则 (如果当前栈顶没有字符的话,显然匹配失败,则直接return false。)用pop方法查看当前栈顶的字符topchar,如果c是’)’但是当前的topchar不是’(‘则匹配失败;同理,如果c是’]’但是当前的topchar不是’[‘则匹配失败;

4,在循环之外,如果栈里还有字符串则匹配失败。

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
import java.util.Stack
public class Solution{
public boolean isVaild(String s){
Stack<Character> stack = new Stack<>(); //声明这个栈
for(int i = 0;i<s.length();i++){ //遍历字符串
char c = s.charAt(i); //查看字符串中第i个字符是什么样子的,存成char c
if(c == '('|| c=='[' || c=='{'
stack.push(c); //如果符合条件,则push压入栈中,以便后期pop出来匹配
else{
if(stack.isEmpty()) //如果没有要匹配的字符串
return false;
char topChar = stack.pop();//pop使元素出栈,将栈顶元素存入topChar中
//如果不能匹配成功
if(c == ')' && topChar != '(')
//如果检索到的符号和pop出的符号无法匹配则失败
return false;
if(c == ']' && topChar != '[')
return false;
if(c == '}' && topChar != '}')
return false;
}
}
//如果栈为空则匹配成功
return stack.isEmpty();
}
}

队列 Quene

特点:先进先出

创建循环队列–对长度取余数

属性:头front,尾tail。front == tail 队列为空;(front+1)%data.length == tail 队列满

几个重要方法

enqueue()方法: 从队尾入队。

1,判断队列是否已经满了(tail转了一圈之后,与front是否相等),若满了则进行扩容。

2,把元素加到队尾(下标为tail)。

3,tail向后移动一位(为了实现循环,因此需要对长度取余数),size数量增加。

1
2
3
4
5
6
7
8
9
public void enqueue(E e){

if((tail+1)%data.length == front) //如果队列已经满了(已经转了一圈了)
resize(2*data.length); //进行扩容

data[tail] = e; //把元素加到队尾
tail = (tail+1)%data.length; //队尾下标加一,并对数组长度取余数,以便循环。
size++;
}

**dequeue()方法:**从队首出队。

1,判断队列是否为空,若为空则抛出异常。

2,把要被删除的元素(下标为front)存入ret中,后删除这个元素(指向null)。

3,front向后移动一位(为了实现循环,因此需要对长度取余数),size数量减少

4,为了节约空间,有必要进行缩容。但是缩容的这个值不应该为0。

1
2
3
4
5
6
7
8
9
10
11
12
public E dequeue(){
if(isEmpty()) //如果队列为空,则抛出异常
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
E ret = data[front]; //把要删除的元素存在ret中
data[front] = null;
front = (front+1) % data.length; //front向后移动一位,为了实现循环,因此对长度取余数
size--;
//进行缩容,以免空间被浪费
if(size == getCapacity()/4 && getCapacity()/2 != 0 )
resize(getCapacity()/2);
return ret;
}

resize()方法:调整队列的容量。

1,新建数组newData

2,通过循环,把旧数组中元素都复制到新数组中。但是,在原来的data中有可能front不是0,可是我们要把它放在新的空间的0的位置,因此存在front个偏移,由于是循环队列i+front会越界,因此加上偏移后还有对长度取余数。

3,把引用变量指向新数组,并设置front为0,tail为size

1
2
3
4
5
6
7
8
public void resize(int newCapacity){
E[] newData =(E[]) new Object[newCapacity+1];
for(int i = 0;i < size;i++)
newData[i] = data[(i+front)%data.length];
data = newData;
front = 0;
tail = size;
}

整体实现——代码实例

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
public class LoopQuene<E>{            //支持泛型
private E[] data;
private int front,tail; //队首和队尾的下标
private int size; //元素的数量

public LoopQuene(int capacity){ //有参的构造方法
data = (E[]) new Object[capacity+1]; //新建一个泛型数组
front = 0; //此时队列中没有元素
tail = 0;
size = 0;
}
public LoopQuene(){ //无参的构造方法
this(10);
}
public int getCapacity(){ //队列的容量
return data.length-1;
}
public boolean isEmpty(){ //是否为空
return front == tail;
}
public int getSize(){ //队列中元素的个数
return size;
}
public void enqueue(E e){ //入队
if((front+1)%data.length == tail)
resize(2*data.length);
data[tail] = e;
tail = (tail+1)%data.length;
size++;
}
public E dequeue(){ //出队
if(isEmpty()) //若队列为空,则抛出异常
throw new IllegalArgument("");
E ret = data[front];
front = (front+1)%data.length; //front向前移动
data[front] = null; //将front对应元素设置为null,真正删除
size--; //元素数量减少

if(getCapacity == size/4 && getCapacity) //缩容,但是缩容的这个值不应该为0
resize(getCapacity/2);
return ret;
}
public void resize(int newCapacity){
E[] newData = (E[])new Object[newCapacity+1];
for(int i =0;i<size;i++) //解释同上
newData[i] = data[(i+front)%data.length];
data = newData;
front = 0 ;
tail = size;
}
public E getFront(){ //得到队首元素
if(isEmpty())
throw new IllegalArgumentException("Queue is empty.");
return data[front];
}
}

Chp2简单排序

冒泡排序

思路:

1,将最小的数据放在数组的最开始(index为0),并将最大数据放在数组的最后(数组下标为nELem-1)。

2,外层for循环的计数器out从数组最后开始,即out等于nElems-1,每次循环一次out减一。index大于out的数据已经排好序了。变量out在每完成一次内部循环(计数器为in)后就左移一位,因此算法就不再处理那些已经排好序的数据了,最终到index为1。

3,内层for从循环计数器in从index 0开始,每循环一次in加一,当in等于out时结束一次循环。在内层for循环体中,数组下标index为in和in+1的两个数据项进行比较,如果下标index为in的数据项大于index为in+1的数据项,则交换两个数据项。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void bubbleSort(){
int out,in;
int temp;
for(out=nElems-1;out>1;out--){
for(in=0;in<out;in++){
if(a[in]>a[in+1]){
temp = a[in];
a[in] = a[in+1];
a[in+1]=temp;
}
}
}
}

选择排序

思路:

1,外层循环用循环变量out,从数组开头开始(index为0)向高位增长。内层循环用循环变量in,从index为out+1开始,同样是向右移位。

2,在每一个in的新位置,数据项a[in]和a[min]进行比较。如果a[in]更小,则min被赋值为in的值。在内层循环的最后,min指向最小的数据项,然后交换out和min指向的数组数据项。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void selectionSort(){
int out,in,min;
int temp;
for(out=0;out<nElems-1;out++){
min = out; //minimum
fot(in=out+1;in<nElems;in++) //inner loop
if(a[in]<a[min]) //if min greater
min = in; //we have a new min
temp = a[out];
a[out] = a[min];
a[min] = temp;
}
}

选择排序的效率

选择排序与冒泡排序执行了相同次数的比较:N*(N-1)/2。对于10个数据项,需要45次比较。但只需要少于10次的交换。当N值很大时,比较的次数是很重要的,结论上选择排序和冒泡排序一样运行了O(N^2)时间。但是,选择排序无疑更快,因为它进行的交换少得多。

插入排序

思路:

在数组中间有一个作为标记元素。在这个标记元素左边的所有元素已经是局部有序的,右边所有元素是无序的

Chp1 Array

创建一个数组HighArray

HighArray中的find()方法 ,用数据项的值作为参数传递,依次查找数组中的每一个数据项。返回值是true或者false,取决于是否找到该数据项。

insert()方法,向数组下一个空位置放置一个新的数据项。一个名为size的字段跟踪记录着数组中实际已有的数据项个数。main()方法不再需要为数组中还有多少数据项而担心。

delete()方法, 查找相应的数据项,当它找到该数据项后,便将所有后面的数据项前移,从而覆盖了待删除的数据项,然后nElem减少一。

dispaly()方法 , 用来显示数组中所有的数据项的值。

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
class HighArray{
private long[] data; //ref to array data
private int size; //number of data items

public HighArray(int max){ //constructor
data = new long[max]; //creat the array
size = 0; //no items yet
}
public boolean find(long searchKey){ //find specified value
int j;
for(j=0;j<nElems;j++){ //for each elements
if(data[j] == searchKey) //found items?
break; //exit loop before end gone to end?
}
if(j == size)
return false; //can't found it
else
return true; // found it
}
public void insert(long value){ //put elements into array
data[size] = value; //insert it
nElems ++; //increamnt size
}
public boolean delete(long value){
int j;
for (j=0 ;j<size ;j++) //look for it
if(value == a[j])
break;
if(j == size)
return false; //can't find it
else
for(int k=j;k< size;k++)
a[k] = a[k+1]; //move higher ones down
size--; //decrement size
return true;
}
public void display(){
for(int j =0;j<nElems;j++) //for each elements
System.out.println(a[j]+" "); //display for it
}
}

add(int index,int e) 方法:把元素添加到指定位置。

1、先判断index是否越界,若越界则抛出异常。

2、如果数组元素个数与长度相等,则需要扩容。

3、通过循环,把数组中从最后一位到index位置的元素都向后挪动一位

4,再把index位置中的元素赋值成e。元素数量增加

1
2
3
4
5
6
7
8
9
10
11
12
13
public void add(int index,int e){
//先判断index是否越界
if(index < 0|| index> size)
throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
//如果数组元素个数与长度相等,需要扩容
if(size == data.length)
resize(2*data.length);
//把数组中从最后到index位置的元素都向后挪动一位,再把index位置中的元素赋值成e
for(int i = size; i >= index ;i--)
data[i+1] = data[i];
data[index] = e;
size++;
}

resize(new capacity)方法:进行数组扩容。新建一个新数组newData,把旧数组中元素都赋值给新数组,最后把引用变量指向新数组。

1
2
3
4
5
6
public void resize(int capacity){
int[] newData = new int[capacity];
for(int i = 0;i< size; i++)
newData[i] = a[i];
a = newData;
}

remove(int index)方法: 删除指定index位置的元素。画图来理解下标范围 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int remove(int index){
if(index <0 || index>size)
throw new IllegalArgumentException("Remove failed. Index is illegal.");
//保存要删除位置的元素,以便返回
int ret = data[index];
//从下标index+1开始到size-1,用后一个位置的元素去覆盖前一个位置的元素,就把index位置元素挤出去了。
for(int i = index+1;i<size;i++)
data[i-1] = data[i];
size--;

//为了避免数组空间有所浪费,因此进行缩容
if(size == data.length/2)
resize(data.length/2);
return ret;
}

removeElement(int e)方法: 从数组中删除指定元素。通过find()方法找到该元素所对应的下标,然后调用remove()方法

1
2
3
4
5
public void removeElement(int e){
int index = find(e);
if(index != -1)//如果没有找到
remove(index);
}

重写toString() 方法。

1,创建StringBuilder实例

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append(String.format("Array: size = %d , capacity = %d\n", size, data.length));
res.append('[');
for(int i = 0 ; i < size ; i ++){
res.append(data[i]);
if(i != size - 1) //如果不是最后一个元素
res.append(", ");
}
res.append(']');
return res.toString();
}

二分查找中的find()方法

二分查找用于有序数组。通过将数组数据项范围不断对半分割来查找特定的数据项。

在一开始设变量lowerBound和upperBound指向数组的第一个和最后一个非空数据项,来确定查找searchKey数据项的范围。然后在while循环中,当前下标curIn被设置为这个范围中间值。

循环中每次将范围缩小一半,最终这个范围会小到无法再分割。在下一条语句中会判断。如果lowerBound比upperBound大,则范围已经不存在了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int find(long searchKey){
int lowerBound = 0;
int upperBound = nElems-1;
int curIn;
while(true){
curIn = (lowerBound+upperBound)/2;
if(a[curIn] == searchKey)
return curIn;
else if(lowerBound>upperBound)
return nElems;
else{
if(a[curIn]<searchKey)
lowerBound = curIn+1; //it's in upper half
else
upperBound = curIn -1; //it's in lower half
}
}
}

大O表示法

算法 大O表示法的运行时间
线性查找 O(N)
二分查找 O(logN)
无序数组的插入 O(1)
有序数组的插入 O(N)
无序数组的删除 O(N)
有序数组的删除 O(N)

小结

Java中的数组是对象,由new操作符创建

无序数组可以提供快速的插入,但查找和删除较慢

将数组封装到类中可以保护数组不被随意更改

类的接口由类的用户可以访问的方法组成

有序数组可以使用二分查找

线性查找需要的时间与数组中数据项的个数成正比

二分查找需要的时间与数组中数据项的个数的对数成正比

大O表示法为比较算法的速度提供了一种更方便的方法

O(1)级时间算法是最友好的,O(logN)次之,O(N)为一般,O(N^2)最差。

protected 访问控制符的理解

首先要记住:protected 的属性和方法可以在本包和子类访问

在这里重点强调子类访问:

  • 只要有继承关系,并且在子类中,就可以用super去访问父类中定义的protected修饰的方法或属性,不管子类和父类是不是在同一个包中,并且如果子类中没有重写父类中的方法和属性的话,用this关键字访问的和用super关键字访问是一样的。
  • 只要是在子类中,也可以通过new出一个子类本身的实例,通过引用变量.属性或方法的形式进行访问。
  • 但是,如果在其他类(包括子类)中,就不允许通过实例化父类或者子类(其他子类,非这个子类本身)对象之后,利用引用变量.属性或方法的形式进行访问。

具体例子
父类:

1
2
3
4
5
6
package test1;
public class ProtectedTest {
protected void say(){
System.out.println("ProtectedTest");
}
}

子类A例子:
在子类中直接调用父类protected属性和方法。

1
2
3
4
5
6
7
8
9
10
package test2;
import test1.ProtectedTest;
public class A extends ProtectedTest {
public void speak(){
say();//ok,等价于this.say(),子对象this可以调用父类方法(属性也一样)
}
public static void main(String[] args) {
A a = new A();
a.say();//ok,子对象a可以调用父类方法
}

子类B的例子:
在其他(子)类(B)中创建非自身子类(A)的实例,调用父类protected方法报错。但是,通过a.speak()可以间接调用父类protected方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package test2;
import test1.ProtectedTest;
public class B extends ProtectedTest{
public void speak(){
say();////ok,等价于this.say(),可以调用父类方法
A a = new A();
a.say();
//此处报错,不允许a调用:The method say() from the type ProtectedTest is not visible
//当父类放到其他包下面的时候,这里报错,因为say方法是protected可见性,跨包不可见,因此不可访问
a.speak(); //同样可以调用say(),但是这里是没有问题的,因为是调用的speak()方法,它不是protected方法,但是此方法在定义时调用了say()
}
public static void main(String[] args) {
B b = new B();
b.say();//ok,子对象b可以调用父类方法
}
}

从上面这个例子可以知道,如果想在其他类中调用父类的protected方法,有两种情况:
1、这个类是拥有protected方法父类的子类
2、先在子类中定义了一个非protected的方法,但是在这个方法中调用了该父类的protected方法,在其他类中通过实例化此子类对象,调用这个方法也能间接达到调用父类protected方法的效果。如果这个子类和这个方法是public的,那么当然跨包的其他类也能调用

return在try-catch-finally语句中的使用

return在try-catch-finally语句中的使用

若在 try或catch语句里面有return语句,finally语句和return语句的执行顺序问题:

  • 若有finally语句,则无论如何,都会执行该语句,在try或catch中的return语句会将它的返回值压入栈内,然后执行finally语句,当finally执行完成后,若finally语句里有return语句,则执行return语句并结束。
  • 若finally没有return语句,则返回被保存的栈里的return语句,再执行。然而,在压栈时候,要注意压入栈内的是什么东西,是值本身还是引用,若是引用则引用的值会改变,若是变量值,则不会改变。

例子

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
public class TestFinal {  
public static void main(String[] args) {
System.out.println("test1:" + testFinal1());
System.out.println("test2:" + testFinal2());
System.out.println("test3:" + testFinal3());
System.out.println("test4:" + testFinal4());
}

static int testFinal1() {
int i = 1;
try {
return i;
} finally {
System.out.println("in testFinal1():finally 肯定会被执行的!");
i = 48;
}
}

static String testFinal2() {
String str = "try";
try {
return str;
} finally {
System.out.println("in testFinal2():finally 肯定会被执行的!");
str = "finally";
}
}

static StringBuilder testFinal3() {
StringBuilder build = new StringBuilder("try ");
try {
return build;
} finally {
System.out.println("in testFinal3():finally 肯定会被执行的!");
build.append("finally");
build = new StringBuilder("你猜我是谁!");
}
}

static String testFinal4() {
try {
return "return in try";
} finally {
System.out.println("in testFinal4():finally 肯定会被执行的!");
return "return in finally";
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
输出是:

in testFinal1():finally 肯定会被执行的!

test1:1

in testFinal2():finally 肯定会被执行的!

test2:try

in testFinal3():finally 肯定会被执行的!

test3:try finally

in testFinal4():finally 肯定会被执行的!

test4:return in finally

结论很明显,finally的语句确实执行了,而且肯定是在方法return之前执行的,而且,如果finally中有return语句的话,方法直接结束。这里需要注意的只有一点:在try中的return语句会将返回结果值压栈,然后转入到finally子过程,等到finally子过程执行完毕之后(没有return),再返回。

下面具体看4个例子:

  • 在testFinal1()中,return i;会将结果i的值,也就是1压入栈。即使在finally中将i修改了(i=48),也不回对已经压入栈里的1造成任何影响。
  • 在testFinal2()中,return str;将str的内容压入栈,比如我们假设str的内容为0x108(只是一个地址值),通过这个地址值我们能找到”try”,那栈里的内容就是0x108。执行str = “finally”,这时候str这个变量的内容可能变为0x237了,这是串”finally”的地址。方法调用结束后,返回的是什么?return时压入栈里的0x108。所以在打印结果时,我们打印的是通过0x108找到的字符串”try”。
  • 在testFinal3()中,return 压栈的是build这个变量的值,比如是0x3579,通过这个值我们可以找到StringBuilder对象。finally语句块中通过append方法对这个对象的内容进行了修改。而build = new StringBuilder(“你猜我是谁!”);让build变量指向了一个新的对象,这时候build的值可能是0x4579了。
    但是,别忘了,原来的StringBuilder对象仍然在0x3579处,而我们压栈的正是0x3579啊!方法返回后,我们得到的返回值0x3579,通过这个引用值找到相应的StringBuilder对象,所以打印的结果是test3:try finally。
  • 在testFinal4()中,finally有return语句,直接返回,方法结束。

Java中int与Integer用==比较详解

int和Integer的区别

1、Integer是int的包装类,int则是java的一种基本数据类型
2、Integer变量必须实例化后才能使用,而int变量不需要
3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
4、Integer的默认值是null,int的默认值是0

先看一道例题判断 true 和 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void testEquals() {
int int1 = 12;
int int2 = 12;

Integer integer1 = new Integer(12);
Integer integer2 = new Integer(12);
Integer integer3 = new Integer(127);

Integer a1 = 127;
Integer a2 = 127;

Integer a = 128;
Integer b = 128;

System.out.println("int1 == int2 -> " + (int1 == int2));
System.out.println("int1 == integer1 -> " + (int1 == integer1));
System.out.println("int1 == integer2 -> " + (int1 == integer2));
System.out.println("integer1 == integer2 -> " + (integer1 == integer2));
System.out.println("integer3 == a1 -> " + (integer3 == a1));
System.out.println("a1 == a2 -> " + (a1 == a2));
System.out.println("a == b -> " + (a == b));
}

结果:

1
2
3
4
5
6
7
1、int1 == int2 -> true
2、int1 == integer1 -> true
3、int1 == integer2 -> true
4、integer1 == integer2 -> false
5、integer3 == a1 -> false
6、a1 == a2 -> true
7、a == b -> false

下面我们就来详细解释一下,为什么是上面的结果。(下面的序号就是对应的是上面的答案序号)

1、int1 == int2 为true,
对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false

2,3、 “int1== integer1” 和 “int1 == integer2” 为true
Integer是int的封装类,当Integer与int进行 ==比较时,Integer就会拆箱成一个int类型,所以还是相当于两个int类型进行比较,这里的Integer,不管是直接赋值,还是new创建的对象,只要跟int比较就会拆箱为int类型(实际上就变为两个int变量的比较),所以就是相等的。

4、integer1 == integer2 -> false,
由于Integer变量实际上是对一个Integer对象的引用,因为new生成的是两个对象,其内存地址不同。这是两个都是对象类型,而且不会进行拆箱比较,所以不等。

5、integer3 == a1 -> false ,
非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是Java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)

6,7、看起来是一模一样的为什么一个是true,一个是false?

1
2
3
4
5
6
7
8
Integer a1 = 127;
Integer a2 = 127;

Integer a = 128;
Integer b = 128;

System.out.println("a1 == a2 -> " + (a1 == a2));
System.out.println("a == b -> " + (a == b));

这是因为Integer作为常量时,对于-128到127之间的数,会进行缓存,也就是说int a1 = 127时,在范围之内,这个时候就存放在缓存中,当再创建a2时,Java发现缓存中存在127这个数了,就直接取出来赋值给a2,所以a1 == a2的。当超过范围就是new Integer()来new一个对象了,所以a、b都是new Integer(128)出来的变量,所以它们不等。

关于Java对于-128到127之间的数,会进行缓存的具体解释:

例如Java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);,而java API中对Integer类型的valueOf的定义如下:

1
2
3
4
5
6
7
public static Integer valueOf(int i){
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high){
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}

因此Java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了

Java引用类型的概念

1.什么是引用类型

引用类型(reference type)指向一个对象,不是原始值,指向对象的变量是引用变量。

在java里面除去基本数据类型的其它类型都是引用数据类型,自己定义的class类都是引用类型,可以像基本类型一样使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyDate {
private int day = 8;
private int month = 8;
private int year = 2008;
private MyDate(int day, int month, int year){...}
public void print(){...}
}
public class TestMyDate {
public static void main(String args[]) {
//这个today变量就是一个引用类型的变量
MyDate today = new MyDate(23, 7, 2008);
}
}

按值传递的重要特点:传递的是值的拷贝,也就是说传递后就互不相关了。第9行的a和第2行的a是两个变量,当改变第2行的a的值,第9行a的值是不变的,所以打印结果是3。

1
2
3
4
main  方法中的a 为 3
test1 方法中的a 为 4

我们把第9行的a称之为实参,第2行的a称之为形参;对于基本数据类型,形参数据的改变,不影响实参的数据。

2、引用类型的赋值

在java编程语言中,用类的一个类型声明的变量被指定为引用类型,这是因为它正在引用一个非原始类型,这对赋值具有重要的意义。如下代码:

1
2
3
4
int x = 7;
int y = x;
String s = "Hello";
String t = s;

四个变量被创建:两个基本数据类型 int 和两个引用类型String。
x的值是7,而这个值被复制到y;x和y是两个独立的变量且其中任何一个的进一步的变化都不对另外一个构成影响。
至于变量s和t,只有一个String对象存在,它包含了文本”Hello”,s和t均引用这个单一个对象。

enter image description here

如果将变量t重新定义为t=”World”;则新的对象World被创建,而t引用这个对象。

enter image description here

3、调用方法时,按值传递和按引用传递的区别

1)按值传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Sample{
int num;
public Sample(int num) {
this.num = num;
}
}

public class Main {
public static void main(String[] args) {
Sample s = new Sample(10);
modify(s.num);
System.out.println(s.num);
}
private static void modify(int num) { //modify方法的参数类型是基本类型变量
num *=2;
}

}

按值传递的重要特点:传递的是值的copy,也就是说传递后就互不相关了。因此,传递到方法modify中的并不是sample类的属性num自身,而是num属性的值的copy。所以,打印结果是10。

2)按引用传递

指的是在方法调用时,传递的参数是按引用进行传递,其实传递的是引用的地址,也就是变量所对应的内存空间的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Sample{
int num;
public Sample(int num) {
this.num = num;
}
}

public class Main {
public static void main(String[] args) {
Sample s = new Sample(10);
modify(s);
System.out.println(s.num);
}
private static void modify(Sample s) { //modify方法的参数类型是引用类型变量
s.num *=2;
}
}

按引用传递的重要特点:调用方法时,传递的并不是对象本身。(print引用类型变量s得到的就是一个内存地址。)传递的是引用instance实例的内存地址,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间,都指向同一个实例)。因此,实例仍然还是一个。所以结果为20。

网上的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 classA {
public int age = 0;
}

public class TempTest {
private void test1(A a) {
a.age = 20;
System.out.printIn("test1方法中的age="+a.age);
}
public static void main(String args[]) {
TempTest t = new TempTest();
A a = new A();
a.age = 10;
t.test1(a);// 这里传递的参数a就是按引用传递
System.out.printIn("main方法中的age="+a.age);
}
}

运行结果如下:test1方法中的age = 20 main方法中的age = 20

用上面的例子来进行分析:

(1)、运行开始,“TempTest t = new TempTest();”,创建了一个A的实例,内存分配示意图如下:

main方法中的a

enter image description here

(2)、第13行:“a.age = 10;”,修改了A实例里面的age的值,内存分配示意图如下:

main方法中的a

enter image description here

(3)、第14行:“t.test1(a);”,是把main方法中的变量a所引用的内存空间地址,按引用传递给test1方法中的a变量。请注意:这两个a变量是完全不同的,不要被名称相同所蒙蔽,但它们指向了同一个A实例。内存分配示意图如下:

enter image description here

(4)、运行第7行,即test1(A a)方法中“a.age = 20;”,为test1方法中的变量a指向A实例的age进行赋值,完成后形成新的内存示意图如下:

enter image description here

此时A实例的age值的变化是由test1方法引起的。