-
1,Java面向对象的特征是什么
封装(增加了代码的复用性)
· 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而言它的内部细节是隐藏的,暴露给外界的只是它的访问方法。属性的封装:使用者只能通过事先定制好的方法来访问数据,可以方便地加入逻辑控制,限制对属性的不合理操作;方法的封装:使用者按照既定的方式调用方法,不必关心方法的内部实现,便于使用;便于修改,增强代码的可维护性。
继承(增加了代码的复用性)
· 从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。在本质上是特殊-一般的关系。子类继承父类,表明子类是一种特殊的父类,并且具有父类所不具有的一些属性或方法。从多种实现类中抽象出一个基类,使其具备多种实现类的共同特性,当实现类用extends关键字继承了基类(父类)后,实现类就具备了这些相同的属性。继承的类叫做子类(派生类或者超类),被继承的类叫做父类(或者基类)。比如猫、狗、虎类的共同特性(吃、叫、跑等)。java通过extends关键字来实现继承,父类中通过private定义的变量和方法不会被继承,不能在子类中直接操作父类通过private定义的变量以及方法。继承避免了对一般类和特殊类之间共同特征进行的重复描述,通过继承可以清晰地表达每一项共同特征所适应的概念范围,在一般类中定义的属性和操作适应于这个类本身以及它以下的每一层特殊类的全部对象。运用继承原则是的系统模型比较简练也比较清晰。
多态(增加了代码的可移植性、健壮性、灵活性)
· 多态必备三个要素:继承、重写、父类引用指向子类对象。例:annimal a=new dog -
2,ArrayList和LinkedList的区别?(数据结构)
ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。
LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。
相对于ArrayList,LinkedList的插入、添加、删除操作速度更快,因为当元素被添加到集合任意位置时,不需要像数组那样重新计算大小或者是更新索引。
相对于ArrayList,LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
1),因为Array是基于索引的数据结构,它使用索引在数组中搜索和读取数据时很快的。Array获取数据的时间复杂度是O(1),但是要删除数据却是开销很大的,因为这需要重排数组中的所有数据。
2)LinkedList相对于ArrayList插入是更快的,因为LinkedList不像ArrayList一样,不需要改变数组的大小,也不需要在数组装满的时候要将所有的数据重新装入一个新的数组,这是ArrayList最坏的一种情况,时间复杂度复杂度是O(n),而LinkedList中插入或删除的时间复杂度仅为O(1)。ArrayList在插入数据时还需要更新索引(除了插入数组的尾部)。
3)类似于插入数据,删除数据时,LinkedList也优于ArrayList。
4)LinkedList需要更多的内存,因为ArrayList的每个索引的位置时实际的数据,而LinkedList中的每个节点中存储的是实际的数据和前后节点的位置。
什么场景下更适合使用LinkedList,而不是ArrayList?
1)你的应用不会随机访问数据。因为如果你需要LinkedList中的第n个元素的时候,你需要从第一个元素顺序数到第n个数据,然后读取数据。
2)你的应用更多的插入和删除元素,更少的读取数据。因为插入和删除元素不涉及重排数据,所以它要比ArrayList要快。 -
3,手写二分查找
①前提:有已排序数组A
②定义左边界L、右边界R,确定搜索范围,循环执行二分查找(3、4两步)
③获取中间索引 M=(L+R)/2
④中间索引的值A[M]与待搜索的值T进行比较
· A[M]==T表示找到,返回中间索引
· A[M]>T,中间值右侧的其它元素都大于T,无需比较,中间索引左边去找,M-1设置为右边界,重新查找
· A[M]<T,中间值左侧的其它元素都小于T,无需比较,中间索引右边去找,M+1设置为左边界,重新查找
⑤当L>R时,表示没有找到,应结束循环
public class BinarySearch {
public static void main(String[] args) {
int []array={1,5,8,11,19,22,31,35,40,45,48,49,50};
int target=48;
int idx=binarySearch(array,target);
System.out.println(idx);
}
public static int binarySearch(int []array,int target){
int n=array.length;
int l=0;
int r=n-1;
int idx=0;
while(l<=r){
idx=l+(r-l)/2;
if(array[idx]<target){
l=idx+1;
}
else if (array[idx]>target){
r=idx-1;
}
else {
return idx;
}
}
return -1;
}
}
- 4,排序
· 常见排序算法:快排、冒泡、选择、插入等
冒泡排序
· ①依次比较数组中相邻两个元素大小,若a[j]>a[j+1],则交换两个元素,两两都比较一遍称为一轮冒泡,结果是让最大的元素排至最后。
· ②重复以上步骤,直到整个数组有序。
优化方式:
· 每轮冒泡时,最后一次交换索引可以作为下一轮冒泡比较次数,如果这个值为零,表示整个数组有序,直接退出外层循环即可。import java.util.Arrays; public class BubbleSort { public static void main(String[] args) { int a[]={5,2,7,4,1,3,8,9}; bublle_v2(a); //System.out.println(Arrays.toString(a)); } public static void bublle_v2(int []a){ int count=a.length-1; while(true){ int cou=0; for(int j=0;j<count;j++){ if(a[j]>a[j+1]){ swap(a,j,j+1); cou=j; System.out.println(j); } } count=cou; System.out.println(Arrays.toString(a)); if(count==0){ break; } } } public static void swap(int []a,int m,int n){ int temp=a[m]; a[m]=a[n]; a[n]=temp; } }
选择排序
①,将数组分为两个子集,排序的和未排序的,每一轮从未排序的子集中选出最小的元素,放入排序子集。
②,重复以上步骤,直到整个数组有序。
与冒泡排序比较:
· 二者平均时间复杂度都是O(n2)
· 选择排序一般要快于冒泡,因为其交换次数少
· 但如果集合有序度高,冒泡优于选择
· 冒泡属于稳定排序算法,而选择属于不稳定排序
import java.util.Arrays;
public class SelectionSort {
public static void main(String[] args) {
int []a={5,3,7,2,1,9,8,4};
selection(a);
}
private static void selection(int[] a){
for(int j=0;j<a.length-1;j++){
int idx=j;
for(int i=j+1;i<a.length;i++){
if(a[idx]>a[i]){
idx=i;
}
}
if(idx!=j){
swap(a,idx,j);
}
System.out.println(Arrays.toString(a));
}
}
private static void swap(int []a,int m,int n){
int temp=a[m];
a[m]=a[n];
a[n]=temp;
}
}
插入排序
文字描述:以升序为例
· ①,将数组分为两个区域,排序区域和未排序区域,每一轮从未排序区域中取出第一个元素,插入到排序区域(需保证顺序)
②,重复以上步骤,直到整个数组有序
优化方式:
①,待插入元素进行比较时,遇到比自己小的元素,就代表找到了插入位置,无需进行后续比较
②,插入时可以直接移动元素,而不是交换元素
与选择排序比较:
①,二者平均时间复杂度都是O(n2)
②,大部分情况下,插入都略优于选择
③,有序集合插入的时间复杂度为O(n)
④,插入属于稳定排序算法,而选择属于不稳定排序
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args) {
int []a={9,3,7,2,5,8,1,4};
insert(a);
System.out.println(Arrays.toString(a));
}
public static void insert(int []a){
int j;
for(int i=1;i<a.length;i++){
int temp=a[i];
for(j=i-1;j>=0;j--){
if(temp<a[j]){
a[j+1]=a[j];
}
else
break;
}
a[j+1]=temp;
}
}
}
希尔排序(知道概念即可)
快速排序:
· 每一轮排序选择一个基准点(pivot)进行分区
· ①,让小于基准点的元素进入一个分区,大于基准点的元素进入另一个分区
· ②,当分区完成时,基准点元素的位置就是其最终位置
· 在子分区内重复以上过程,直至子分区元素个数少于等于1,这体现的是分而治之的思想。
方式一:单边循环快排
①,选择最右元素作为基准点元素
②,j指针负责找到比基准点小的元素,一旦找到则与i进行交换
③,i指针维护小于基准点元素的边界,也是每次交换的目标索引
④,最后基准点与i交换,i即为分区位置
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args) {
int a[]={5,3,7,2,9,8,1,4};
quick(a,0,a.length-1);
}
public static void quick(int []a,int l,int n){
if(l>=n)return;
int p = partition(a, l, n);
quick(a,l,p-1);
quick(a,p+1,n);
}
private static int partition(int a[],int l,int n){
int pv=a[n];
int i=l;
int j;
for (j = l; j < n; j++) {
if(a[j]<pv){
if(i!=j){
swap(a,i,j);
}
i++;
}
}
if(i!=n){
swap(a,i,n);
}
System.out.println(Arrays.toString(a));
return i;
}
public static void swap(int []a,int m,int n){
int temp=a[m];
a[m]=a[n];
a[n]=temp;
}
}
方式二:双边循环快排
①,选择最左元素作为基准点元素
②,j指针负责从右向左找比基准点小的元素,i指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至i,j相交。
③,最后基准点与i(此时i与j相等)交换,i即为分区位置。
import java.util.Arrays;
public class QuickSort2 {
public static void main(String[] args) {
int a[]={5,3,7,2,9,8,1,4};
quick(a,0,a.length-1);
}
public static void quick(int []a,int l,int n){
if(l>=n)return;
int p = partition(a, l, n);
quick(a,l,p-1);
quick(a,p+1,n);
}
private static int partition(int a[],int l,int h){
int pv=a[l];
int i=l;
int j=h;
while(i<j){
while(i<j&&a[j]>pv){
j--;
}
while(i<j&&a[i]<=pv){
i++;
}
if(i<j){
swap(a,i,j);
}
}
swap(a,l,i);
System.out.println(Arrays.toString(a));
return i;
}
public static void swap(int []a,int m,int n){
int temp=a[m];
a[m]=a[n];
a[n]=temp;
}
}
双边循环注意:
· ①,基准点在左边,并且要先j(从右向左)后i(从左向右)。
· ②,while(i<j && a[j] >pv) j–;
· ③,while(i<j && a[i] <=pv) i++;
快排特点:
①,平均时间复杂度是O(nlog2n),最坏时间复杂度O(n2)
②,数据量较大时,优势非常明显
③,属于不稳定排序
ArrayList
ArrayList扩容机制
· 1)ArrayList()会使用长度为零的数组
· 2)ArrayList(int initialCapacity)会使用指定容量的数组
· 3)public ArrayList(Collection<? extends E> C)会使用c的大小作为数组容量
· 4)add(Object o)首次扩容为10,再次扩容为上次容量的1.5倍
· 5)addAll(Collection c)没有元素时,扩容为Math.max(10,实际元素个数),有元素时为Math.max(原容量1.5倍,实际元素个数)
fail-fast与fail-safe
· ArrayList是fail-fast的典型代表,fail-fast 一旦发现遍历的同时其它人来修改,则立即抛异常
· CopyOnWriteArrayList是fail-safe的典型代表,遍历的同时可以修改,fail-safe 发现遍历的同时其它人来修改,应当能有应对策略,例如牺牲一致性来让整个遍历运行完成
ArrayList和LinkedList区别:
ArrayList:
· ①,基于数组,需要连续内存
· ②,随机访问快(指根据下标访问)
· ③,尾部插入,删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
· ④,可以利用CPU缓存,局部性原理,提升相邻元素被访问的机会(cpu计算时,会从内存中取数据,直接取会浪费时间,可以借助cpu缓存,将要取的元素和其相邻的元素一并取到cpu缓存中,之后要用到数据时,可以直接从缓存中取数据,这样就节省了时间)
LinkedList
· ①,基于双向链表,无需连续内存
· ②,随机访问慢(要沿着链表遍历)
· ③,头尾插入删除性能高
· ④,占用内存多
HashMap
底层数据结构,1.7和1.8有何不同?
· ① 1.7 数组+链表,1.8 数组+(链表|红黑树)
为何要用红黑树,为何一上来不树化,树化阈值为何是8,何时会树化,何时会退化为链表?
答:
1)红黑树用来避免Dos攻击,防止链表超长时性能下降,树化应当是偶然情况
· ①hash表的查找,更新的时间复杂度是O(1),而红黑树的查找,更新时间复杂度是O(log2n),TreeNode占用空间也比普通Node的大,如非必要,尽量还是使用链表。
· ②hash值如果足够随机,则在hash表内按泊松分布,在负载因子0.75的情况下,长度超过8的链表出现概率是0.00000006,选择8就是为了让树化概率足够小
2)树化的两个条件:链表长度超过树化阈值;数组容量>=64
3)退化情况1:在扩容时如果拆分树时,树元素个数<=6则会退化链表
4)退化情况2:remove树节点时,若root、root.left、root.right、root.left.left有一个是null,也会退化为链表。
索引如何计算?hashCode都有了,为何还要提供hash()方法?数组容量为何是2的n次幂?
任何一个对象都提供了hashCode方法。
①计算对象的hashCode(),再进行调用HashMap的hash()方法进行二次哈希,最后(capacity-1)&得到索引
②二次哈希()是为了综合高位数据,让哈希分布更为均匀,防止超长链表的产生。
③计算索引时,如果是2的n次幂可以使用位运算代替取模,效率更高;扩容时hash&oldCap==0的元素留在原来位置,否则计算新位置=旧位置+oldCap
④但①,②,③都是为了配合容量为2的n次幂时的优化手段,例如Hashtable的容量就不是2的n次幂,并不能说哪种设计更优,应该是设计者综合了各种因素,最终选择了使用2的n次幂作为容量。
⑤,想要追求效率,则数组容量选择2的n次幂;想要追求数据分布相对均匀,则选择质数作为数组容量。
介绍put方法流程,1.7和1.8有何不同?
· ①,HashMap是懒惰创建数组的,首次使用才创建数组
· ②,计算索引(桶下标)
· ③,如果桶下标还没人占用,创建Node占位返回
· ④,如果桶下标已经有人占用
· ④-1,已经是TreeNode走红黑树的添加或更新逻辑
· ④-2,是普通Node,走链表的添加或更新逻辑,如果链表长度超过阈值,走树化逻辑。
· ⑤,返回前检查容器是否超过阈值,一旦超过进行扩容
· ⑥,不同
· ⑥-1,链表插入节点时,1.7是头插法,1.8是尾插法
· ⑥-2,1.7是大于等于阈值且没有空位时才扩容,而1.8是大于阈值就扩容
· ⑥-3,1.8在扩容计算Node索引时,会优化。