您好,登錄后才能下訂單哦!
我們要學習的第一個數據結構就是數組,數組中很多值得挖掘。
數組基礎
把數據碼成一排進行存放
數組中索引從0開始,Java語法中要求數組存放同一類型的元素,可以通過中括號下標的方式取到元素。
這樣可以看到Main中有的方法。
package cn.mtianyan;
public class Main {
public static void main(String[] args) {
// 必須傳入長度
int[] arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
System.out.println("=========");
int[] scores = new int[]{100,99,96};
for (int i = 0; i < scores.length; i++) {
System.out.println(scores[i]);
}
System.out.println("=========");
for (int score : scores) {
System.out.println(score);
}
System.out.println("=========");
scores[0] = 92;
for (int score : scores) {
System.out.println(score);
}
}
}
數組可以這樣遍歷,是因為它實現了可遍歷接口。
Java為我們提供的數組,其中非常重要的一部分就是它的索引。索引可以有語義,也可以沒有語義,2可以代表學號,但也可以將其視為沒有語義。
數組最大的優點: 快速查詢,scores[2]直接插到2號索引的。因此數組最好應用于"索引有語義"的情況,查學號為2的學生成績。
但并非所有有語意的索弓都適用于數組,如×××號: 1101031985121 66666;依次為索引,這得開辟很大的內存空間,空間浪費。
數組也可以處理“索引沒有語意”的情況。我們在這一章,主要處理“索引沒有語意”的情況數組的使用。
索引沒有語意,如何表示沒有元素? capacity 和 size的區別: 數組size是3,capacity是8。
如何添加元素(超過size如何處理)?如何刪除元素(挪位)? Java中的數組并沒有這些方法,我們需要基于java的數組,二 次封裝屬于我們自己的數組類。
我們自己做的是動態數組(內部依然使用java數組實現),Java的是靜態數組。
增刪改查,有的數據結構不一定四個齊全。capacity是數組最多可以裝多少元素,和實際能裝多少元素是沒有關系的。
package cn.mtianyan;
public class Array {
private int[] data;
private int size;
/**
* 帶容量參數構造函數
*
* @param capacity 數組容量
*/
public Array(int capacity) {
data = new int[capacity];
size = 0;
}
/**
* 默認構造函數
*/
public Array() {
this(10);
}
/**
* 靜態數組入參構造函數
*
* @param data 傳入靜態數組
*/
public Array(int[] data) {
this.data = data;
}
/**
* 獲取數組元素個數
*
* @return size 數組元素個數
*/
public int getSize() {
return size;
}
/**
* 獲取數組的容量
*
* @return capacity 獲取容量
*/
public int getCapacity(){
return data.length;
}
/**
* 判斷數組是否為空
*
* @return 是否為空
*/
public boolean isEmpty(){
return size == 0;
}
}
向數組中添加元素
向數組末尾添加元素
size指向data中第一個為空位置。因此添加元素時只需要添加到arr[size],并size++即可。(注意添加元素判滿)
/**
* 向所有元素末尾添加一個新元素。
*
* @param e 添加的元素
*/
public void addLast(int e){
// if (isFull()){
// throw new IllegalArgumentException("AddLast failed. Array is Full");
// }else {
// data[size] =e; // data[size++] =e;
// size++;
// }
add(size,e);
}
/**
* 向索引0號位置添加元素
*
* @param e 添加的元素
*/
public void addFirst(int e){
add(0,e);
}
/**
* 在index位置插入一個新元素e
*
* @param index 插入位置索引
* @param e 插入元素
*/
public void add(int index,int e){
if (isFull())
throw new IllegalArgumentException("AddLast failed. Array is Full");
// 大于size就不是緊密排列了
if (index<0 || index>size){
throw new IllegalArgumentException("AddLast failed. Required index<0 or index>size ");
}
else {
// 從哪開始挪呢: 從size-1這個元素(size本身是指向空的),挪到哪個呢,index位置的這個元素也是要挪的。
for (int i=size-1; i>=index; i--){
data[i+1] = data[i]; // 后一個等于前一個,從數組最后一個元素開始。
// 極端值驗證: size 1 index 0;(i=0;i>=0;i--)會執行一次data[1]=data[0].正確。
}
data[index] = e;
size++;
}
}
77插入,后面的元素都要向后挪動。
這里注意必須從100這里,向后挪,否則會造成值覆蓋。
在數組中查詢元素和修改元素
/**
* 打印數組信息及遍歷元素。
*
* @return 數組信息和元素遍歷結果
*/
@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();
}
package cn.mtianyan;
public class ArrayTest {
public static void main(String[] args) {
Array array = new Array(20);
for (int i = 0; i < 10; i++) {
array.addLast(i);
}
System.out.println(array);
}
}
array.add(1,100);
System.out.println(array);
array.addFirst(-1);
System.out.println(array);
取出某一個元素。
/**
* 傳入索引,獲取該位置元素
*
* @param index 要獲取的元素索引
* @return 返回該位置數組元素
*/
public int get(int index){
if (index<0 || index>=size){
throw new IllegalArgumentException("Get failed. Required index<0 or index>=size ");
}else {
return data[index];
}
}
通過get方法用戶無法使用到數組后半截沒有使用到的空間,通過封裝保證。
System.out.println(array.get(array.getSize()-1));
/**
* 傳入索引和元素值,將該位置元素設置為傳入值
* @param index 索引
* @param e 傳入值
*/
public void set(int index,int e){
if (index<0 || index>=size){
throw new IllegalArgumentException("Set failed. Required index<0 or index>=size ");
}else {
data[index] = e;
}
}
array.set(0,-99);
System.out.println(array);
數組中的包含,搜索和刪除元素
如果要刪除索引為1的元素,從88開始,后面都要往前挪,挪size-index次。只需要size--,對于最后一個位置的元素不用做其他操作,因為用戶也訪問不到。
/**
@return 包含 true; 不包含 false
*/
public boolean contains(int e){
for (int i = 0; i < size; i++) {
if (data[i] == e){
return true;
}
}
return false;
}
/**
@return 不存在 -1; 存在 i
*/
public int find(int e){
for (int i = 0; i < size; i++) {
if (data[i] == e){
return i;
}
}
return -1;
}
public int[] findAll(int e){
int[] tempArray=new int[size];
int index = 0;
for (int i = 0; i < size; i++) {
if (data[i] == e){
tempArray[index] = i;
index++;
}
}
int[] indexArray=new int[index];
for (int i = 0; i < index; i++) {
indexArray[i] = tempArray[i];
}
return indexArray;
}
注意findAll中使用tempArray臨時對象的作用: 同時將int數組,與它的size一起傳遞了出來。
System.out.println("===============加入重復元素后數組如下===================");
array.add(3,3);
array.add(array.getSize()-1,9);
System.out.println(array);
System.out.println("================包含 尋找測試===========================");
System.out.println(array.contains(-99));
System.out.println(array.contains(-100));
System.out.println("3的index: "+array.find(3));
int [] tmpArr = array.findAll(3);
for (int i : tmpArr) {
System.out.print(i+" ");
}
System.out.println();
/**
@return 該索引位置元素值
*/
public int remove(int index){
// 判空,空數組 removeFirst index=0,size=0,index>=size異常。空數組 removeLast index=-1 index<0異常;
if (index<0 || index>=size){
throw new IllegalArgumentException("Remove failed. Required index<0 or index>=size ");
}else {
int ret = data[index];
for (int i=index+1;i < size;i++){
// 從哪個元素開始挪,從index位置的后一個元素開始,挪到哪個元素結束,挪到size-1(因此沒等號)
data[i-1] = data[i]; // 前一個等于后一個
}
size--;
return ret;
}
}
/**
@return 刪除的元素值
*/
public int removeFirst(){
return remove(0);
}
/**
@return 刪除的元素值
*/
public int removeLast(){
return remove(size-1);
}
/**
@return 刪除是否成功
*/
public boolean removeElement(int e){
int index = find(e);
if (index != -1){
remove(index);
return true;
}
return false;
}
/**
@return 刪除成功與否
*/
public boolean removeAllElement(int e){
int[] indexArray = findAll(e);
if (indexArray.length != 0){
for (int i = 0; i < indexArray.length; i++) {
remove(indexArray[i]-i); // 此處注意-i的巧妙
}
return true;
}
return false;
}
重難點代碼在于remove(indexArray[i]-i);此處很可能會被忽略,造成異常。因為數組每刪除一個,原本獲取到的臨近后一個元素的index值應該-1;臨近后兩個則應該-2;以此類推。
System.out.println("================開始刪除測試========================");
System.out.println(array);
array.remove(3); // 刪除一個3
System.out.println(array);
array.removeElement(1); // 刪除1
System.out.println(array);
System.out.println("=====刪除index3 后 刪除元素1如上====");
array.removeAllElement(9);
System.out.println(array);
System.out.println("=====刪除所有9=====");
array.addFirst(-99);
array.removeAllElement(-99);
System.out.println(array);
System.out.println("=====首位添加-99,后刪除所有-99 結果如上=====");
array.add(4,99);
array.add(5,99);
array.addFirst(99);
array.addLast(99);
array.add(7,99);
System.out.println(array);
System.out.println("=====上面為刪除99前,下面為刪除99后=====");
array.removeAllElement(99);
System.out.println(array);
使用泛型
我們上面實現的數組目前只能存放int類型,但是我們需要的是可以承載多種類型,甚至自定義對象的容器。Java泛型可以解決這個問題。
使用泛型: 讓我們的數據結構可以放置“任何”數據類型,不可以是基本數據類型,只能是類對象。
java中的八種基本類型: boolean , byte, char , short , int , long , float , double;
每個基本數據類型都有對應的包裝類: Boolean , Byte , Char , Short, Int , Long , Float , Double。自動的拆包,解包。
public class Array<Element>
public class Array<E>
為類名后面添加泛型類型標識,此處的類型標識可以自定義。
private E[] data;
這里無法直接通過E實例化。只能通過間接的Object再轉換。
data = (E[]) new Object[capacity];
/**
@param data 傳入靜態數組
*/
public Array(E[] data) {
this.data = data;
}
public void add(int index,E e){
public void addLast(E e){
public void addFirst(E e){
public boolean contains(int E){
for (int i = 0; i < size; i++) {
if (data[i].equals(E)){
return true;
}
}
return false;
}
這里注意,兩個對象之間的比較要使用equals方法,將所有的與成員數組類型相關的都改了。
public E remove(int index){
// 判空,空數組 removeFirst index=0,size=0,index>=size異常。空數組 removeLast index=-1 index<0異常;
if (index<0 || index>=size){
throw new IllegalArgumentException("Remove failed. Required index<0 or index>=size ");
}else {
E ret = data[index];
for (int i=index+1;i < size;i++){
// 從哪個元素開始挪,從index位置的后一個元素開始,挪到哪個元素結束,挪到size-1(因此沒等號)
data[i-1] = data[i]; // 前一個等于后一個
}
size--;
data[size] = null;
return ret;
}
}
data[size]還指著一個值不過用戶訪問不到而已,新的元素添加會自動覆蓋。使用泛型后,data數組中存放的是類對象的引用, size-- 后data[size]依然指向一個引用,引用就涉及到空間釋放的問題,Java有自動垃圾回收機制,但如果 data[size]仍存放引用,就不會被自動垃圾回收。data[size] = null;(不必須,可以被新對象覆蓋)
如果不置為null,它會被稱為loitering Objects,沒有用但垃圾回收機制不回收。 loitering Objects != memory leak 為了程序優化,手動去除更好。
Main中改動
Array<Integer> array = new Array(20);
Array<Integer> array = new Array<>(20);
即使不改,也是可以正常運行的。但是我們最好加上更清楚的看出類型,jdk1.7之后不用再后面再重復一次。
package cn.mtianyan;
public class Student {
private String name;
private int score;
public Student(String studentName, int studentScore){
name = studentName;
score = studentScore;
}
@Override
public String toString(){
return String.format("Student(name: %s, score: %d)", name, score);
}
public static void main(String[] args) {
Array<Student> arr = new Array();
arr.addLast(new Student("mtianyan1", 100));
arr.addLast(new Student("mtianyan2", 66));
arr.addLast(new Student("mtianyan3", 88));
System.out.println(arr);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
Student student = (Student) o;
return score == student.score &&
(name.equals(student.name));
}
@Override
public int hashCode() {
return Objects.hash(name,score);
}
重寫hashCode和equals方法。
System.out.println("================start=======================");
System.out.println(arr);
arr.remove(1);
Student stu1 = new Student("mtianyan1", 100);
Student stu3 = new Student("mtianyan3", 88);
arr.removeElement(stu1);
System.out.println(arr);
System.out.println("==============================================");
System.out.println(arr);
arr.removeAllElement(stu3);
System.out.println(arr);
動態數組
Java靜態數組容量有限,固定。我們要將其改造成一個可伸縮的。
不夠用了,就開辟一個新的數組,容量是原來的二倍(4變為8),然后把原來數組的內容進行復制過來(size不變),最后將data指向新的數組。這里要遍歷的把每個值都搞過去,對于性能消耗大不大呢,本章后兩節會討論。
if (isFull())
throw new IllegalArgumentException("AddLast failed. Array is Full");
// 大于size就不是緊密排列了
if (index<0 || index>size){
throw new IllegalArgumentException("AddLast failed. Required index<0 or index>size ");
}
坐標非法依然拋出異常,但是如果滿了,我們的動態數組將不會拋出異常。
if (isFull())
resize(2*data.length);
這里的2倍,是優于擴大固定大小值的。2的選擇是我們自己決定的,Collection中ArrayList中選取了1.5。
private void resize(int newCapacity) {
E[] newData = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
package cn.mtianyan;
public class ArrayTestDynamic {
public static void main(String[] args) {
Array<Integer> array = new Array<>();
for (int i = 0; i < 10; i++) {
array.addLast(i);
}
System.out.println(array);
array.addFirst(99);
System.out.println(array);
array.addLast(100);
System.out.println(array);
}
}
刪除時壓縮數組容量
public E remove(int index){
// 判空,空數組 removeFirst index=0,size=0,index>=size異常。空數組 removeLast index=-1 index<0異常;
if (index<0 || index>=size){
throw new IllegalArgumentException("Remove failed. Required index<0 or index>=size ");
}else {
E ret = data[index];
for (int i=index+1;i < size;i++){
// 從哪個元素開始挪,從index位置的后一個元素開始,挪到哪個元素結束,挪到size-1(因此沒等號)
data[i-1] = data[i]; // 前一個等于后一個
}
size--;
data[size] = null;
if (size == data.length /2 )
resize(data.length /2);
return ret;
}
}
這里設置一個閾值,當數組一半為空時,容量減半。
package cn.mtianyan;
public class ArrayTestDynamic {
public static void main(String[] args) {
Array<Integer> array = new Array<>();
for (int i = 0; i < 10; i++) {
array.addLast(i);
}
System.out.println(array);
array.addFirst(99);
System.out.println(array);
array.addLast(99);
System.out.println(array);
array.removeAllElement(99);
System.out.println(array);
for (int i = 0; i < 5; i++) {
array.removeElement(i);
}
System.out.println(array);
}
}
簡單的復雜度分析
簡單的時間復雜度分析 感性認識 考研: 細致推導
O(1), O(n) , O(lgn) , O(nlogn) , O(n^2);
大O描述的是算法的運行時間和輸入數據之間的關系
public static int sum(int[] nums){
int sum = 0;
for(int num: nums) sum += num;
return sum;
}
通常我們說上面這個代碼的算法,時間復雜度是O(n)的。n是nums中的元素個數;算法和n呈線性關系,線性關系表現在一次方。
num 數字越多,算法執行時間越長。
為什么要用大O,叫做O(n)?
因為我們忽略了常數,實際時間 T=c1*n + c2,大O是一個大概的; 對于每一個數所花的時間是c1,int return sum時間是c2。
因此,無論c1,c2是多少,只要符合線性關系都是O(n)的算法。
即使c1,c2很小,但是是二次方,因此是O(n2),性能差于O(n)。沒錯,理論性能差,但是對于一些小數據,如n=10,反倒是O(n2)的要執行更快。
漸進時間復雜度,描述n趨近于無窮的情況。(3000)高階算法常數低反而快于低階算法: 高級排序算法,歸并排序,快速排序,都可以對于小數據轉為插入排序,獲得10-15%優化,插入排序高階但常數小。
因為是趨向于無窮的情況,因此可以將其中的低階項忽略掉。
分析動態數組的時間復雜度
添加操作
addLast(e) // O(1) 末尾添加,直接賦值,其他什么也不做。
O(1)意味著它與我們的數組規模沒有關系,不管數組n有多大,addLast都能在常數時間內完成,此時不考慮size變化。
addFirst(e) // O(n) 前面添加,所有元素后挪
n個元素,就要后挪n-1次。
add(index, e)
與index有關。因此可以這樣分析:index這么多取值的概率是相等的,嚴格計算需要一些概率論知識,求出時間期望;平均來看是O(n/2)
添加操作綜合來看,整體是O(n)級別的。
在算法復雜度分析上通常關注的是最壞的情況,有一些特例,下小節講個特例。
resize // O(n)級別
刪除操作:
修改操作:
set(index, e) // O(1)級別
查找操作:
get(index) // O(1)
contains(e) // O(n)
find(e) // O(n)
數組最好情況是已知索引。
如果只對最后一個元素操作,addLast RemoveLast O(1)依然是O(n),因為resize把全部元素重新遍歷賦值一遍。看起來resize好像是一個性能很差的操作。
對于resize的分析,使用最差情況是不合理的。下一小節使用均攤復雜度進行分析。
均攤復雜度和防止復雜度的震蕩
添加操作
最壞情況下,尾部添加就擴容,從O(1)變成了O(n)級別;但是我們不可能每次操作都resize,10次才會觸發一次resize。
假設當前capacity=8,并且每一次添加操作都使用addLast:
9次addLast操作,觸發resize,總共進行了17次基本操作。大約平均每次addLast操作,進行2次基本操作。
推廣開: 假設capacity = n, 第n+1次addLast,觸發resize,總共進行2n+1次基本操作,平均每次addLast操作,進行2次基本操作。
這就將resize的時間平均給了每一次addLast操作,這樣均攤下來,時間復雜度是O(1)級別的!和n沒有關系。
在這個例子里,這樣均攤計算,比計算最壞情況有意義。均攤復雜度 amortized time complexity
同理,我們看removeL ast操作,均攤復雜度也為0(1)
復雜度震蕩。
我們單獨看removeLast和addLast,它的復雜度為O(1)
但當兩個在capacity臨界點,一次add 一次remove,會不停的觸發resize。存在該情況,出現問題的原因: removeLast時resize過于著急
(Eager)
解決方案: Lazy
當數組size是當前容量的1/4,進行縮容量,縮1/2,則還有1/4空間可以加元素。size == capacity/4時,才將capacity減半。
Lazy方式,懶一下,算法性能更好。后面講到線段樹時還有例子:當更懶,改善算法性能(懶機制代碼更多)。
if (size == data.length /4 && data.length/2 !=0)
resize(data.length /2);
因為是整數除法,所以當length為1時,會造成等于0,而我們無法new一個容量為0的數組。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。