数据的存储物理结构和逻辑结构
按物理结构:
①:连续的存储空间:数组
元素相邻,在内存中开辟连续的存储空间
缺点:内存吃紧,给数组开辟一整块空间时比较难找。
优点:访问速度相对较快,因为有了首地址,根据下标,直接就可以找到对应的元素。
②:非连续的存储空间:链式
元素不一定相邻,在内存中不需要开辟连续的存储空间。
缺点:访问速度相对数组较慢,要从头开始遍历。
优点:不需要空间连续,可以插空存储。、
按逻辑结构:
①:线性:数组、链表、队列(数组或链表)、栈(数组或链表)
②:非线性:树(二叉树等)、图… …
数组
在java中有三种引用数据类型:类、接口、数组。
引用数据类型在引用变量中存放的是地址。
数组的定义:一种线性表数据结构,它用一组连续的内存空间存储一组具有相同类型的数据。(C/C++完全符合数组的标准定义,无论存储的是基本数据类型还是结构体、对象,在数据中都是连续存储的)
在java中,基本数据类型数组符合数据结构中数组的定义,数组中的数据类型是完全相同的,并且存储在连续的内存空间中。
但对象数组的存储格式已经和C/C++中对象数组的存储格式不一样了,在Java中,对象数组中存储的是对象在内存中的地址(堆中的地址),而非对象本身。对象本身在内存(堆)中并不是连续存储的,而是散落在各个地方的。
数组的定义与初始化
int[][] arr={1,2,3,4};
Student []arr=new Student[3];
arr[0] =new Student("郭超",21);
//可以直接用若干个一维数组初始化一个二维数组,这些一维数组的长度不尽相同。
int [][]a={{1},{1,2},{1,2,3}};
System.out.println(a[0].length);//1
System.out.println(a[1].length);//2
System.out.println(a[2].length);//3
注意:对于char型数组a, System.out.println(a)不会输出数组a的引用而是输出数组a的全部元素,例如:
char b[]={'我','叫','郭','超'};
System.out.println(b);//我叫郭超
查看println方法可以发现,System.out.println有多种形参传入,可以直接将char数组内容打印出来
特点
特点:①:数组长度固定
②:需要额外的变量来记录数组的有效元的个数
③:扩容或维护元素的个数需要大量的工作,效率低下。
有关数组的常用方法:
数据的增删改查往往伴随着数组长度的改变,或者数据移动等。
Arrays.copyOf(original, newLength)//常用于数组扩容
System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);//常用于数组元素移动
Arrays.copyOf(original, newLength)//常用于数组扩容
//传回新数组,改变新数组中的元素值,不会影响原来的数组。
int[] arr1 = {1, 2, 3, 4, 5};
int[] newarr = Arrays.copyOf(arr1, 10);
for(int i = 0; i < newarr.length; i++)
System.out.print(newarr[i] + " ");
//结果:1 2 3 4 5 0 0 0 0 0
System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);//常用于数组元素移动
/*src:源数组;
srcPos:源数组要复制的起始位置;
dest:目的数组;
destPos:目的数组放置的起始位置;
length:复制的长度.
*/
int[] arr = { 1, 2, 3, 4, 5 };
int[] newarr = { 5, 6,7, 8, 9 };
System.arraycopy(arr, 1, newarr, 0, 3);
//int[] newarr = { 2, 3, 4, 8, 9 };
容器能否完全替代数组?
1、Java ArrayList无法存储基本数据类型,如int、long,需要封装为相应的包装类,而自动装箱和拆箱有一定的性能消耗,因此,如果特别关注性能,或者希望使用基本数据类型,就可以选用数组。
2、如果数据大小事先已知,并且对数据的操作非常简单,用不到 ArrayList提供的大部分方法,那么可以直接用数组。
3、对于多维数组的定义,
例如: ArrayList< ArrayList<Object>>
,
Object[][] array
会更加直观。
链表
定义:通过“指针”将一组零散的内存空间(节点)串联起来使用。
这样可以避免创建数组的时候一次性申请过大的内存空间而导致有可能创建失败的问题。
优点:按下标可以随之访问,插入、删除方便。
单链表节点:
class Node{
Object data;
Node next;//下一个节点
}
双链表节点:
class Node{
Node pre;//前节点
Object data;
Node next;//后节点
}
二叉树节点:
class TreeNode{
TreeNode parent;父节点
Object data;
TreeNode left;//左节点
TreeNode right;//右节点
}
单链表的增删改查:
public class SingleLinkedList {
private Node first;
private int total;
private class Node{
Object data;
Node next;
//这里data,next没有私有化,是希望在外部类中可以不需要get/set,而是直接“结点对象.data","结点对象.next"使用
Node(Object data, Node next){
this.data = data;
this.next = next;
}
}
//添加节点
public void add(Object obj){
/*
* (1)把obj的数据,包装成一个Node类型结点对象
* (2)把新结点“链接”当前链表的最后
*/
//这里新结点的next赋值为null,表示新结点是最后一个结点
Node newNode = new Node(obj, null);
//当前新结点是第一个结点,说明newNode是第一个
if(first == null){
first = newNode;
}else{
//先找到目前的最后一个,把新结点链接到它的next中
Node node = first;
while(node.next != null){
node = node.next;
}
//退出循环时node指向最后一个结点,把新结点链接到它的next中
node.next = newNode;
}
total++;
}
//返回所有节点的data
public Object[] getAll(){
//(1)创建一个数组,长度为total
Object[] all = new Object[total];
//(2)把单向链表的每一个结点中的data,拿过来放到all数组中
Node node = first;
for (int i = 0; i < total; i++) {
all[i] = node.data;
node = node.next;
}
return all;
}
//删除节点
public void remove(Object obj){
if(obj == null){
//(1)先考虑是否是第一个
if(first!=null){//链表非空
//要删除的结点正好是第一个结点
if(first.data == null){
//让第一个结点指向它的下一个
first = first.next;
total--;
return;
}
//要删除的不是第一个结点
Node node = first.next;//第二个结点
Node last = first;
while(node.next!=null){//这里不包括最后一个,因为node.next==null,不进入循环,而node.next==null是最后一个
if(node.data == null){
last.next = node.next;
total--;
return;
}
last = node;
node = node.next;
}
//单独判断最后一个是否是要删除的结点
if(node.data == null){
//要删除的是最后一个结点
last.next = null;
total--;
return;
}
}
}else{
//(1)先考虑是否是第一个
if(first!=null){//链表非空
//要删除的结点正好是第一个结点
if(obj.equals(first.data)){
//让第一个结点指向它的下一个
first = first.next;
total--;
return;
}
//要删除的不是第一个结点
Node node = first.next;//第二个结点
Node last = first;
while(node.next!=null){//这里不包括最后一个,因为node.next==null,不进入循环,而node.next==null是最后一个
if(obj.equals(node.data)){
last.next = node.next;
total--;
return;
}
last = node;
node = node.next;
}
//单独判断最后一个是否是要删除的结点
if(obj.equals(node.data)){
//要删除的是最后一个结点
last.next = null;
total--;
return;
}
}
}
}
//查询节点
public int indexOf(Object obj){
if(obj == null){
Node node = first;
for (int i = 0; i < total; i++) {
if(node.data == null){
return i;
}
node = node.next;
}
}else{
Node node = first;
for (int i = 0; i < total; i++) {
if(obj.equals(node.data)){
return i;
}
node = node.next;
}
}
return -1;
}
}
链表相关代码技巧
1、理解指针或引用的含义
2、警惕指针丢失会内存泄露
例如:在a和b之间插入节点x
错误写法:
a.next=x;
x.next=a.next;
正确写法应该交换一下两行代码顺序。
3、利用哨兵简化代码
4、留意边界条件和特殊情况
链表为空?
链表只包含一个节点?
链表只包含两个节点?
要处理的节点是特殊节点?头结点、尾结点?
5、举例画图,辅助思考
数组与链表对比
数组和链表是两种截然不同的两种内存组织方式。不能仅从时间复杂度进行比较。
1、数组使用连续的存储空间存储数据,我们可以有效地利用CPU的缓存机制,预读数组中的数据,提高访问效率。而链表在内存中并不是连续存储的,因此,没办法预读,对CPU的缓存不友好。
2、数组的缺点是大小固定,一经声明就要占用整块的连续存储空间。声明过大,系统可能没有足够的连续内存空间分配给他,就会抛Out of memory;声明过小,可能出现不够用的问题,需要扩容,把原数组中的数据复制过去,非常耗时。而链表不存在这个问题。
3、相比链表,数组更加适合内存紧缺的开发场景。因为链表中的每个节点都需要额外的存储空间来存储next指针,因此内存消耗更多。而且对于链表的频繁插入、删除还会导致频繁的内存申请和释放,容易产生内存碎片,对于java而言,就可能会导致频繁的垃圾回收(GC),也会影响程序性能。