数组和链表
No.1 线性表
线性表是将数据按照一定规则排列起来的数据,有前继,有后继。一般的,可以将所有的数据结构分为线性结构和非线性结构,线性结构中有,数组,链表,栈,对列。
线性结构定义:n个数据元素的有序(次序)集合,其特征为:
1.集合中必须存在唯一的一个“第一个元素”。
2.集合中必须存在唯一的一个“最后一个元素”。
3.除了最后元素之外,其他元素必须有唯一的“后继”。
4.除了第一个元素外,其他元素必须有唯一的“前驱”。
No.2数组基础
数组是一种用连续的内存空间存储相同类型的线性数据结构的工具。
相同数据类型的数据,每个数据占据内存空间的大小也是固定的,所以整个数组占据内存的空间也是固定的,根据数据量的大小来决定。
No.3 数组开辟空间的过程
数组产生地址空间是在栈内存中,开辟内存空间是在堆内存中。数组很特别,例如存入int类型的数据的时候,int类型占4个字节,在开辟了地址空间后,每个数据依次存入数组后都是以4个字节的地址增加存入数据,首地址在初始化定义数组的时候,就会被分配内存地址。
数组中有一个特别的寻址公式,假设首元素的地址为baseAddress 为array[0],要查找数组中3号元素的内存地址,使用公式baseAddress+i*dataSize,其中i变量是要查找的第几号元素,dataSize是数据的大小,占据多少个字节。
No.4 数组的特点
1.数组查询的时间复杂度为O(1)
怎么理解?因为数组的长度是固定的,不会随着要查询的下标而影响整个代码的执行时间,只需要通过寻址公式就能找到要查询的元素,所以很简单,时间复杂度就是O(1)
2.数组的查询和删除操作的复杂度是O(n)
类比,当一个数组中需要插入一个元素,首先得考虑到元素插入的位置,在尾部插入,时间复杂度肯定是O(1),在数组中间插入的话,看具体插入的位置,中间插入的复杂度是O(n),通过计算,算出平均时间复杂度也是O(n).
3.提高数组删除效率的方法,打标记方法(Jvm中垃圾回收中核心的算法)
在一个数组中为了提高删除的效率,可以将要删除的数据打上标记,在某些特定的条件下,我们追求不一定是数组中数据的连续性,如果我们使用多次的删除操作放在一起的话,是不是删除的效率就很提高很多?
打标记删除操作就是为了提高数组的删除效率,在一个数组中有以下几个元素,a1,a2,a3,a4,a5,a6. 若要删除的元素是a1,a2,a5,可以将 a1,a2,a5打上标记,然后删除标记后统一将数据往前挪,这样可以一次性将数据删除,增强了数组的删除效率。
No.5 实战面试题(leetcode11:盛最多水的容器)
1.暴力解法,列举出所有面积存在的可能性,进行暴力求解。
public class Solution{
public int maxArea(int [] arr){
// 定义最大面积max
int max = 0;
// 双层循环求出最大面积
for(int i = 0;i<arr.length;i++){
for(int j = i+1;j<arr.length;j++){
// 定义出最大面积的求法
int area = (j-i)*Math.min(arr[i],arr[j]);
// 更新面积
max=area>max?area:max;
}
}
return max;
}
}
注:这种解法就真的是暴力求解的方法,用第一个i下标的值(长度)去乘上下一个j下标对应的值,这样可以算出每一个围成的图形面积,再更新面积后可以求出最大面积。(为什么是用最短的那个作为长度来乘宽度?因为类比木桶装水的原理是取决于最短的那根,而不是最长的那根。)
2.双指针思想(左右指针,夹逼思想)
public class Solution(){
public int maxArea(int[] arr){
// 定义最大面积max
int max = 0;
// 定义双指针
int i = 0; // 从数组中的第一个元素开始
int j = arr.length-1;// 从数组中最后一个元素开始
// 判断当i往前走,j往回走,两个不会相遇
while(i!=j){
int area = (j-i)*Math.min(arr[i],arr[j]);
if(arr[i]<arr[j]){
i++;
}else{
j--;
}
max=area>max?area:max;
}
return max;
}
}
理解:一开始两个指针一个指向开头一个指向结尾,此时容器的底是最大的,接下来随着指针向内移动,会造成容器的底变小,在这种情况下想要让容器盛水变多,就只有在容器的高上下功夫。 那我们该如何决策哪个指针移动呢?我们能够发现不管是左指针向右移动一位,还是右指针向左移动一位,容器的底都是一样的,都比原来减少了 1。这种情况下我们想要让指针移动后的容器面积增大,就要使移动后的容器的高尽量大,所以我们选择指针所指的高较小的那个指针进行移动,这样我们就保留了容器较高的那条边,放弃了较小的那条边,以获得有更高的边的机会。
No.6 双指针思想之快慢指针(leetCode 283 移动零)
给一个指定数组,例如 int arr [] = {1,2,0,0,3,5},设计代码将数组中的元素重新排序,并且非零元素在前,0排在最后。
public class Solution{
public void moveZeros(int[] arr){
// 先判断数组是否为空
if(arr == null || arr.length<2){
return;
}
// 在不为空的情况下
int i = 0;
int j = 0;
for(;i<arr.length;i++){
if(arr[i]!= 0){
arr[j]=arr[i];
j++;
}
}
// 当依次排序后,剩下末尾的j的值为0.并且要对j坐标的元素赋值
while(j<arr.length){
arr[j]=0;
j++;
}
}
}
仔细思考后发现,j的值可不可以在一次循环内全部进行完,也就是i经过一次循环之后,j坐标上的值已经是非0元素了,i是快指针,j是慢指针,所以i在跳出循环之后,j的坐标依然是小于i的,所以可以考虑改进此代码。
public class Solution{
public void moveZeros(int[] arr){
// 判断数组是否为空
if(arr == null || arr.length<2){
return;
}
// 不为空时
int i = 0;
int j = 0;
int temp = 0;
for(;i++;i<arr.length){
if(arr[i]!=0){
temp = arr[j];
arr[j]=arr[i];
arr[i]=temp;
j++;
}
}
}
}
这样下来一次循环就解决了整个问题!
No.6 动态数组
动态数组的提出:
1.java中底层用数组来存储数据,但是存储机制是不能进行容量扩容的,而且插入数据和删除数据都很麻烦,因此容器要屏蔽底层的数组操作细节,支持数据的查询,插入,删除等操作。
2.java的数组一旦被申请了之后是不能改变容量的大小的,提出有容器的支持,来支持扩容机制。
下面就是手写ArrayList作为容器,底层原理是基于数组的扩容。
List接口:
package com.itcast.List;
public interface List {
/**
* 返回容器中元素的个数
* @return 返回int类型的元素个数
*/
int size();
/**
* 判断容器是否为空
* @return 返回布尔类型判断
*/
boolean isEmpty();
/**
* 传入一个数据判断数据在数组中索引的位置
* @param o 传入的参数o
* @return 返回o在数组中的位置
*/
int indexOf(int o);
/**
* 判断是否包含传入的元素
* @param o 传入的参数o
* @return 返回布尔类型的数据
*/
boolean contains(int o);
/**
* 将数据添加到容器末尾,并判断是否添加成功。
* @param o 传入的参数o
* @return 返回布尔类型数据
*/
boolean add(int o);
/**
* 在指定位置添加元素
* @param index 被指定的索引位置
* @param element 添加的元素element
*/
void add(int index,int element);
/**
* 在指定位置设置元素的值
* @param index 指定的索引位置
* @param element 修改的参数
* @return 返回被设置后的元素
*/
int set(int index,int element);
/**
* 删除元素
* @param o 参数
* @return 返回被删除的元素
*/
int remove(int o);
/**
* 获取容器中的元素
* @param o 参数
* @return 返回获取到的元素
*/
int get(int o);
/**
* 清空容器
*/
void clear();
}
发现实现一个简单的底层代码真的不同意哈,必须得先搞清楚底层原理,那么来了,下次面试问到了ArrayList怎么实现的?我肯定会啦!一定要会自己手写,不看代码的情况下写出来!
ArrayLis实现代码:
package com.itcast.List;
public class ArrayList implements List {
// 定义一个数组
private int [] elementDate;
// 定义数组的大小
private int size;
// 定义默认长度
private final int DEFAULT_CAPATICY=10;
// 使用构造方法初始化数组
public ArrayList(){
elementDate = new int[this.DEFAULT_CAPATICY];
}
// 使用构造方法创建指定大小数组
public ArrayList(int capaticy){
if(capaticy>0){
elementDate= new int[capaticy];
}else {
throw new IllegalArgumentException("capaticy is error"+capaticy);
}
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public int indexOf(int o) {
for(int i = 0;i<size;i++){
if (elementDate[i]==o){
return i;
}
}
return -1;
}
@Override
public boolean contains(int element) {
// 复用indexOf方法,如果返回-1则没有,返回其他数据表示有。
return indexOf(element)>-1;
}
@Override
public boolean add(int element) {
// 在添加之前确定数组是否容量够,不够则要进行扩容。
ensureCapacity(size+1);
// 添加元素
this.elementDate[size]=element;
size++;
return true;
}
@Override
public void add(int index, int element) {
// 索引检查
indexCheck(index);
// 检查容量
ensureCapacity(size+1);
// 添加数据
System.arraycopy(this.elementDate,index,this.elementDate,index+1,size-index);
this.elementDate[index]=element;
// size+1
size++;
}
@Override
public int set(int index, int element) {
// 索引检查
indexCheck(index);
// 保存没有被设置之前的数据
int oldValue= this.elementDate[index];
// 指定位置添加
this.elementDate[index]=element;
return oldValue;
}
@Override
public int remove(int index) {
// 索引检查
indexCheck(index);
// 保存移除之前指定索引位置的元素
int oldValue = this.elementDate[index];
// 移除操作
System.arraycopy(this.elementDate,index+1,this.elementDate,index,size-index-1);
// 总体前移一位,覆盖o位置上的元素,但是最后一个索引位置上的元素仍然还在,必须做清理
this.elementDate[size-1]=0;
size--;
return oldValue;
}
@Override
public int get(int index) {
// 索引检查
indexCheck(index);
// 获取数据
return this.elementDate[index];
}
@Override
public void clear() {
// 遍历数组,每个元素赋值为0,并且清空数组的长度
for (int i = 0;i<size;i++){
this.elementDate[i]=0;
}
// 清空数组长度
size=0;
}
@Override
public String showList() {
StringBuilder st = new StringBuilder();
if(size==0){
st.append("list为空!");
}else {
st.append("list=[");
// 遍历elementData
for (int i = 0;i<size;i++){
if(i==size-1){
st.append(this.elementDate[i]+"]");
}else {
st.append(this.elementDate[i]+",");
}
}
}
// st.append("]");
return st.toString();
}
private void ensureCapacity(int minCapaticy) {
if(minCapaticy>elementDate.length){
grow(minCapaticy);
}
}
private void grow(int minCapaticy) {
int oldCapacity = this.elementDate.length;
int newCapacity = oldCapacity + oldCapacity >> 1;
if (newCapacity<minCapaticy){
newCapacity = minCapaticy;
}
// 实现扩容
copyOf(newCapacity);
}
private void copyOf(int newCapacity) {
int[] newArray = new int [newCapacity];
// 复制数据
System.arraycopy(this.elementDate,0,newArray,0,size);
// 将旧的数组废弃,指向新的数组,同时jvm会回收旧的数组占用的内存。
this.elementDate = newArray;
}
private void indexCheck(int index) {
if(index < 0 || index > size){
throw new IndexOutOfBoundsException("index bound "+"index="+index+"size="+size);
}
}
}
测试代码(手写的,实现了所有的功能!)
package com.itcast.TestArrayList;
import com.itcast.List.ArrayList;
public class ArrayListTest {
public static void main(String[] args) {
// 初始化数组长度
ArrayList list = new ArrayList(10);
// 查看数组长度
System.out.println(list.size());
// 添加元素
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
// 查看数组长度
System.out.println(list.size());
// 继续添加元素
list.add(6);
list.add(7);
list.add(8);
list.add(9);
list.add(10);
list.add(11);
// 查看数组长度和数组
System.out.println(list.showList());
System.out.println(list.size());
// 删除9号索引位置的元素
list.remove(9);
System.out.println(list.showList());
System.out.println(list.size());
// 指定8号索引位置添加元素为23
list.add(8,23);
System.out.println(list.showList());
System.out.println(list.size());
// 将8号索引位置的元素的值设为24
list.set(8,24);
System.out.println(list.showList());
// 获取到8号索引位置的值
System.out.println(list.get(8));
// 移除8号索引位置的元素,并且查看数组长度和遍历数组
list.remove(8);
System.out.println(list.showList());
System.out.println(list.size());
// 清空数组,查看数组和数组长度
list.clear();
System.out.println(list.size());
System.out.println(list.showList());
}
}
No.7 实现动态数组的小总结
1.扩容!怎么实现扩容的一个机制,面试的时候肯定说啊就是默认容量满了之后,再扩大到之前的1.5倍,但是怎么进行扩容的呢?面试官让你手写扩容代码怎么办?所以得搞懂原理,当需要添加一个元素进去的时候,捏一个sureCapacity(size+1)的方法,实现sureCapacity的方法,首先要判断minCapacity>element.size?,则实现扩容机制,定义旧的容量为oldCapacity=element.length,在旧的capacity上增加:int newCapacity = oldCapacity+oldCapacity>>1;如果newCapacity<minCapacity,则将newCapacity=minCapacity,再实现数据的转移操作,创建capyOf(newCapacity)的方法,在方法中,定义一个新的数组,int[] newArray = new int [capacity],所有的步骤大概就是这样的一个思路。
2.检查索引是否合理的方法,在添加,删除中,必须得判断当前索引位置是否合理,定义checkIndex(index)的方法,如果index<0 || index>size,都是不合理的!
3.ArrayList在开发中用的很多,是因为不考虑存储的数据是否为基本类型,那么对于数组,只能存储基本的数据类型,不能存储引用数据类型,所以,就像老师总结的,进行底层的网络框架开发可能使用到数组的例子多。
总结一下学习算法的经验:一道题起码要刷十遍,而且学算法每天都要抽时间来学习,敲!理解后一个个敲出来! 没有坚持学是不行的哦!
有不对的地方还希望大家共同指正!