arraylist转int数组_五千字的数组拓展,面试官对我竖起大拇指喊停
目錄
- 為什么數組下標從0開始?
- 數組定義
- 為什么這么下定義?
- 定義數組的三種方式
- 從 ArrayList 源碼看數組增刪改查
- 初始化
- 增加
- 刪除
- 修改
- 查找
- 數組和容器
- 數組時間復雜度
- 數組插入,刪除優化
- 容器替代數組?
- 字節高頻算法題:移動零
- 算法發散
?
為什么數組下標從0開始?
這個問題上大學第一課C語言的時候我就疑惑,沒有接觸過計算機之前,數數都是從1開始的呀,一只羊兩只羊三只羊,別睡著了。
參考原因如下:
推導得到第i個元素地址公式:
a[i]_address = first_address + i * data_type_size
如果從1開始,推導得到第i個元素地址公式: a[i]_address = first_address + (i-1) * data_type_size
即多了一次-1操作,對于CPU來說,就是多了一次減法指令
數組定義
數組是一種線性表結構,它用一組連續的內存空間,來存儲一組具有相同類型的數據。
「線性表」:具有像線一樣性質的表。即線性表上的數據只有前后關系,數組,鏈表,隊列,棧這樣的都是前后關系的線性表結構,樹和圖這樣的前后左右都有關系的即是非線性表結構。
為什么這么下定義?
一般下定義都是留下了最精煉的字來概括內容,就像一部好的電影沒有一句廢話,下面來分析一下數組定義。
「連續」:正是因為連續的內存空間,所以我們能推算出每個元素的地址,假設一個數組有五個元素,起始地址為00,那么后面元素地址一次為01,02,03,04,別人一問你第五個元素地址,你立馬可以告訴她是04,這正是因為數組的內存空間是一段連續的空間。
然而如果這五個元素存放在鏈表里,那么你就不能立馬告訴別人第五個元素的地址是04了,你要先找到第一個元素取得第二個元素的地址,然后取得第三個元素的地址,一直找下去找到最后一個元素,就是因為鏈表存儲的空間不是連續的,鏈表元素里面除了數據本身還需要多存放下一個元素的地址,通過這種方式來找下一個元素,如果要同時知道鏈表前后是誰就需要雙鏈表了。
注:正是因為數組需要連續的內存空間,所以定義數組的時候都需要指定數組的初始大小,要不然會報錯。JAVA容器類ArrayList底層是Object[]數組實現的,數組指定的初始大小在JDK1.8之前是10,JDK1.8時候變成了0。
「相同類型」:試想一下,你一個數組,一會兒放個int類型,一會兒放個long類型,那么上面提到的內存連續也拯救不了你。你讓計算機咋搞呢,int類型占四個字節,long類型占八個字節(64位操作系統下),計算機是把四個字節看成一個元素,還是八個字節當做一個元素呢,要知道所需存儲空間不同地址不同呀,即使你內存連續都不能根據下標統一尋址了。
「因此,數組兩大特性:」連續內存空間,相同類型元素。數組一切的一切,都是基于這兩個的,基于這兩大特性,數組實現了最大的優點:隨機存取,我們很多時候使用數組都是貪圖這個優點。
定義數組的三種方式
初始化數組主要分為靜態初始化和動態初始化,無論哪一種都需要指定數組大小:
靜態初始化:定義數組時候,開辟空間的同時設置內容,一次性初始化完成;
動態初始化:數組先開辟空間,再使用索引進行內容賦值;
從 ArrayList 源碼看數組增刪改查
感覺純粹看數組的增刪改挺無趣的,我們每個人只要靜下心來都可以實現數組的增刪改查,極客算法里面通過看JAVA的 ArrayList 源碼的方式來看數組增刪改,我覺得挺不錯的:
一來可以看看設計者們怎么封裝的,感受感受優秀代碼設計;
二來可以熟悉熟悉源碼,更加清楚天天用的 ArrayList 底層實現,可以看到有什么值得平時注意的。
以下都是基于JDK1.8,選取ArrayList是因為這個我們平時用的最多。
初始化
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {// 序列化idprivate static final long serialVersionUID = 8683452581122892189L;// 默認初始的容量private static final int DEFAULT_CAPACITY = 10;// 一個空對象private static final Object[] EMPTY_ELEMENTDATA = new Object[0];// 一個空對象,如果使用默認構造函數創建,則默認對象內容默認是該值private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];// 當前數據對象存放地方,transient表明當前對象不參與序列化transient Object[] elementData;// 當前數組長度private int size;// 數組最大長度private static final int MAX_ARRAY_SIZE = 2147483639;// 方法開始 } 復制代碼默認構造函數:
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;/** 也就是實現了 Object[] elementData;elementData = new Object[0] ,即new了一個空的對象數組,數組長度是0 **/} 復制代碼增加
ArrayList 添加了四種添加方法:
- add(E element)
- add(int i , E element)
- addAll(Collection)
- add(int index, E element)
數組末尾追加元素 add(E element)
public boolean add(E e) {ensureCapacityInternal(size + 1); // Increments modCount!!elementData[size++] = e;return true;} 復制代碼ensureCapacityInternal() 確保添加的元素有地方存儲,size+1,默認size為0,+1保證數組下標為size+1這個地方可以存儲新元素,下面的 elementData[size++] = e 進行新的元素追加到數組并且上面的保證使其賦值不會數組越界;
private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;}private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);} 復制代碼minCapacity 為增加元素時所需最小長度數組容量大小;
下面第一次add時候,將當前elementData數組的長度用 Math.max 變為10,即第一次add時候 將數組長度 minCapacity 變為默認初始容量10;(jdk1.8以前都是直接初始化的時候指定this(10)直接指定默認容量大小)
非第一次add的時候,minCapacity 為原數組的長度+1:
如果所需的最小長度大于了現有數組長度,那么現在的數組容量肯定是不夠的,需要進行擴容;
modCount 是從 abstractList 里面繼承過來的值,用于迭代器Iterator的操作次數記錄;
private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;// 右移運算符等價于除以2,如果第一次是10,擴容之后的大小是15int newCapacity = oldCapacity + (oldCapacity >> 1);if (newCapacity - minCapacity < 0)newCapacity = minCapacity;// 考慮邊界問題,數組最大容量為2的31次方,int為四個字節,每個字節8位if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData = Arrays.copyOf(elementData, newCapacity);}private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;} 復制代碼擴容,如果添加元素所需最小容量minCapacity(即當前的數組已使用空間(size)加1)大于數組長度,則增大數組容量,擴大為原來的1.5倍。(右移一位相當于除以2)
數組最大容量為2的31次方,數組長度length屬性是int,int為四個字節,每個字節8位,2G內存,沒有人會喪心病狂搞這么大數組吧!
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {@SuppressWarnings("unchecked")T[] copy = ((Object)newType == (Object)Object[].class)? (T[]) new Object[newLength]: (T[]) Array.newInstance(newType.getComponentType(), newLength);System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));return copy;}public static native void arraycopy(Object src, int srcPos,Object dest, int destPos,int length); 復制代碼Arrays.copyOf追蹤下去代碼,確保有足夠的容量之后,使用System.arraycopy 將舊數組拷貝到新的數組.
數組中間插入一個元素
public void add(int index, E element) {// 判斷index 是否有效rangeCheckForAdd(index);// 計數+1,并確認當前數組長度是否足夠,和上面的追加一樣ensureCapacityInternal(size + 1); // Increments modCount!!System.arraycopy(elementData, index, elementData, index + 1,size - index); // 將index 后面的數據都往后移一位elementData[index] = element; // 設置目標數據size++;}private void rangeCheckForAdd(int index) {if (index > size || index < 0)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));} 復制代碼需要插入的位置(index)后面的元素統統往后移動一位,然后將新值插入。
整個插入過程:
刪除
ArrayList 中提供了 五種刪除數據的方式:
- remove(int i)
- remove(E element)
- removeRange(int start,int end)
- clear()
- removeAll(Collection c)
修改
這個簡單,需要改哪個,直接 data[index] = 4 重新賦值就可以
查找
數組支持隨機訪問,根據下標隨機訪問的時間復雜度為O(1)。
但是這并不代表數組的查找時間復雜度是O(1),即使是排好序的數組,你用二分查找,時間復雜度也是O(logn),這是兩個概念。
數組和容器
數組時間復雜度
如果在數組的末尾插入元素,那就不需要移動數據了,這時的時間復雜度為O(1)。
但如果在數組的開頭插入元素,那所有的數據都需要依次往后移動一位,所以 最壞時間復雜度是O(n)。
因為我們在每個位置插入元素的概率是一樣的,所以平均情況時間復雜度為(1+2+...n)/n=O(n)。
數組插入,刪除優化
上面數組的插入和刪除效率是很低的,正是因為數組是連續的空間內存,而插入和刪除的時候改變了數組的空間內存,為了維護連續的內存空間所以要進行數組元素的移動。
具有這個特性,就要維護他,比如紅黑樹具有查找快速的特點,插入和刪除的時候就必須要通過各種左旋右旋操作來維護紅黑樹的平衡,其實是一樣的道理。
插入優化
如果數組中的數據是有序的,我們在某個位置插入一個新的元素時,就必須按照剛才的方法搬移插入位置之后的數據。
但是,我們開發中,如果數組中存儲的數據并沒有任何規律,數組只是被當作一個存儲數據的集合。在這種情況下,如果要將某個數據插入到第i個位置,為了避免大規模的數據搬移,還有一個簡單高效的辦法就是,直接將第i位的數據搬移到數組元素的最后,把新的元素直接放入第i個位置(具體如下圖)。
利用這種處理技巧,在特定場景下,在第i個位置插入一個元素的時間復雜度立即降為了O(1),快排就用到了這個處理思想。
刪除優化(標記清除算法)
標記清除算法 是JVM垃圾回收里面用到的核心算法,具體的可以看公眾號《阿甘的碼路》里面,有關垃圾回收機制相關的文章。
如果數組中數據不要求連續的情況下,我們將多次刪除操作集中在一起執行,只做標記清除工作而不進行真正的刪除,然后統一進行刪除,刪除的效率會提高很多不用進行數據多次的搬遷。
容器替代數組?
容器優點:
容器缺點: 裝箱拆箱有一定的性能損耗
數組優點:
字節高頻算法題:移動零
審題: 保持非零元素相對順序,指的是元素在數組里面的相對順序,而不是讓保證元素相對大小。
思路:
這里使用的Python的 api 還是很方便的,代碼也很清晰明了,思路簡單。JAVA就做不到這樣add然后remove,集合的實現方式不一樣,不信的話可以進行實現,你會發現有很多報錯。
缺點: 空間復雜度很高,每次remove其實都需要移動此元素后面所有的元素。
創建兩個指針i和j,第一次遍歷的時候指針j用來記錄當前有多少非0元素。即遍歷的時候每遇到一個非0元素就將其往數組左邊挪,第一次遍歷完后,j指針的下標就指向了最后一個非0元素下標。
第二次遍歷的時候,起始位置就從j開始到結束,將剩下的這段區域內的元素全部置為0。
時間復雜度:O(n)
空間復雜度:O(1)
在原數組上面進行操作,所有的非0元素往前移動,0自然在后面了
- j記錄要填入的非零元素位置,遇到非0元素就挪動到j位置上;
- 遍歷整個數組,遇到nums[i]==0 時候不處理;如果非0的時候,則把nums[i]的非0元素和nums[j]上的0元素互換,調換位置;
- j始終指向的是下一個非0元素;
很抽象,移動零最優解圖解如下:
省了一次遍歷,借鑒了快排的思路:
快排:快速排序首先要確定一個待分割的元素做中間點x,然后把所有小于等于x的元素放到x的左邊,大于x的元素放到其右邊;
移動零:我們可以用0當做這個中間點,把不等于0(注意題目沒說不能有負數)的放到中間點的左邊,等于0的放到其右邊。
總結
以上是生活随笔為你收集整理的arraylist转int数组_五千字的数组拓展,面试官对我竖起大拇指喊停的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: tv英语域名注册_企业邮箱十万个为什么—
- 下一篇: c# vscode 配置_使用VSCod