如今安卓面試,對於算法的問題也愈來愈多了,要求也愈來愈多,特別是排序,基本必考題,並且還動不動就要手寫,因此陸續要寫算法的文章,也正好當本身學習。o(╥﹏╥)o面試
Android技能書系列:算法
Android基礎知識數組
Android技能樹 — Android存儲路徑及IO操做小結post
數據結構基礎知識
算法基礎知識
本文主要講算法基礎知識及排序算法。
咱們常常聽到說XXX排序算法是穩定性算法,XXX排序算法是不穩定性算法,那穩定性究竟是啥呢?
舉個最簡單的例子:咱們知道冒泡排序中最重要的是二二進行比較,而後按照大小來換位置:
if(arr[j]>arr[j+1]){
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
複製代碼
咱們能夠看到這裏的大小斷定是前一個比後一個大,就換位置,若是相等就不會進入到if的執行代碼中,因此咱們二個相同的數挨在一塊兒,不會進行移動,因此冒泡排序是穩定的排序算法,可是若是咱們把上面的代碼改動一下if裏面的判斷:
if(arr[j]>=arr[j+1]){
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
複製代碼
咱們添加了一個等號,那這個時候就不是穩定排序算法了,由於咱們能夠看到相等的時候它也換了位置了。
證實某個排序是不穩定很簡單,好比你只要傳入{2,2},只要換了位置就是不穩定,證實不穩定只要一種狀況下是不穩定的,那麼就是不穩定排序算法。
複雜度包括了時間複雜度和空間複雜度,可是一般咱們單純說複雜度的時候都指時間複雜度。
用1+2+3+...+100爲例: 普通寫法:
int sum = 0, n = 100;//執行1次
for(int i =1 ; i <= 100 ; i++){ //執行n+1次
sum = sum + i; //執行n次
}
Log.v("demo","sum:"+sum); //執行1次
複製代碼
咱們能夠看到一共執行了2n+3次。(咱們這裏是2*100+3)
高斯算法:
int sum = 0,n = 100; //執行1次
sum = (1+n)*n /2; //執行1次
Log.v("demo","sum:"+sum); //執行1次
複製代碼
咱們能夠看到一共執行了3次。
可是當咱們的n很大的時候,好比變成了1000,第一種算法就是2*1000+3,可是第二種仍是3次。
在進行算法分析時,語句總的執行次數T(n)是關於問題規模n的函數,進而分析T(n)隨n的變化狀況並肯定T(n)的數量級。算法的時間複雜度,也就是算法的時間量度,記做:T(n) = O(f(n))。它表示隨問題規模n的增大,算法執行時間的增加率和f(n)的增加率相同,稱做算法的漸近時間複雜度,簡稱爲時間複雜度。其中f(n)是問題規模n的某個函數。
咱們已經根據上面的解釋看到了: T(n) = O(f(n));
因此第一種是:2*n + 3 = O(f(n)),第二種是3 = O(f(n));咱們能夠看到是增加率和f(n)的增加率相同。
咱們之前學高數都知道:好比f(x) = x^3 + 2x ,隨着x的變大,其實基本都是x^3的值,而2x的的值後面影響愈來愈小,因此有高階的時候,其餘低階均可以隨着x的變大而忽略,同理前面的相乘的係數也是同樣,因此:
那咱們上面的第一種就變成了O(n)(ps:只保留最高位,係數變爲1,),第二種變爲了O(1)(ps:常數都變爲1)
常見的時間複雜度:
最壞/最好/平均狀況
好比咱們玩猜數字,讓你在1-n範圍內猜某個數字,而你是從頭至尾報數,若是猜的數正好是1,則最好狀況下複雜度是1,若是猜的數是n,則最壞是n,平均的話就是n/2。排序也是同樣,好比2,1,3,4,5,6,7,8,9你只須要調換2,1就能夠,可是若是是9,8,7,6,5,4,3,2,1讓你從小到大排序,你須要調換不少次。引用《大話數據結構》中的例子,好比你要計算某一年是否是閏年,你能夠寫一個算法:
if(year%4==0 && year%100 != 0){
System.out.println("該年是閏年");
}else if(year % 400 == 0){
System.out.println("該年是閏年");
}else{
System.out.println("該年是平年");
}
複製代碼
可是若是你在內存中存儲了一個2150元素的數組,而後這個數組中是index是閏年的數組設置爲1,其餘設置爲0,這樣別人好比問你2000年是否是閏年,你直接查看該數組index爲2000裏面的值是否是1便可。這樣經過一筆空間上的開銷來換取了計算時間。
一個算法的優劣主要從算法的執行時間和所須要佔用的存儲空間兩個方面衡量。
排序方法分爲兩大類: 一類是內部排序, 指的是待排序記錄存放在計算機存儲器中進行的排序過程;另外一類是外部排序, 指的是待排序記錄的數量很大,以致於內存一次不能容納所有記錄,在排序過程當中尚需對外存進行訪問的排序過程。
冒泡排序算法的運做以下:(從後往前) 1.比較相鄰的元素。若是第一個比第二個大,就交換他們兩個。 2.對每一對相鄰元素做一樣的工做,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。 3.針對全部的元素重複以上的步驟,除了最後一個。 4.持續每次對愈來愈少的元素重複上面的步驟,直到沒有任何一對數字須要比較。
咱們以最簡單的數組{3,2,1}來看:
咱們能夠看到一種二個大的藍色步驟(ps:3 - 1),而後每一個藍色裏面的交換步驟是一步步變少(ps:2,1)。
因此咱們就知道是二個for循環了:
/**
咱們的藍色大框一共執行(3-1)次,
也就是(nums.length -1)次
*/
for (int i = length -1; i > 0; i--) {
/**
咱們藍色大框交換步驟從(3-1)次開始,
且每一個藍色大框裏面的交換步驟在逐步減一,
正好就是上面的藍色大框的(i變量)
*/
for (int j = 0; j < i; j++) {
//對比邏輯代碼
}
}
複製代碼
而後在裏面加上判斷,若是前一個比後一個大,交換位置便可。
public void bubbleSort(int[] nums) {
//傳進來的數組只有0或者1個元素,則不須要排序
int length = nums.length;
if (length < 2) {
return;
}
for (int i = length -1; i > 0; i--) {
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
int data = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = data;
}
}
}
}
複製代碼
咱們會先找一個關鍵數據,一般爲第一個數,好比咱們這裏的5,而後把數字小於5的數字都放在5的左邊,大於5的數字都放在5右邊,而後對於左邊的數字使用相同的方法,取第一個爲關鍵數據,對其排序,而後一直這麼重複。
僞代碼:
quickSort ( nums ){
//小於2個的數組直接返回,由於個數爲0或者1的確定是有序數組
if(nums.length < 2){
return nums;
}
//取數組第一個數爲參考值
data = nums[0];
//左邊的數組
smallNums = (遍歷nums中比data小的數)
//右邊的數組
bigNums = (遍歷nums中比data大的數)
//使用遞歸,對左邊和右邊的數組分別再使用咱們寫的這個方法。
return quickSort(smallNums) + data + quickSort(bigNums);
}
複製代碼
咱們一步步來看如何實現具體的代碼:(我會先根據思路寫一個步驟不少的寫法,用於介紹,再寫一個好的。)
其實要實現功能,這個很簡單,咱們能夠新建二個數組,而後再徹底遍歷整個原始數組,把比參考值小的和大的分別放入二個數組。
//取第一個數爲參考值
int data = nums[0];
//咱們先獲取比參考值大的數及小的數各自是多少。
int smallSize = 0, bigSize = 0;
for (int i = 1; i < nums.length; i++) {
if (nums[i] <= data) {
smallSize++;
} else {
bigSize++;
}
}
//創建相應的數組,等會用來放左邊和右邊的數組
int[] smallNums = new int[smallSize];
int[] bigNums = new int[bigSize];
//遍歷nums數組,把各自大於或者小於參考值的放入各自左邊和右邊的數組。
int smallIndex = 0;
int bigIndex = 0;
for (int i = 1; i < nums.length; i++) {
if (nums[i] > data) {
bigNums[bigIndex] = nums[i];
bigIndex++;
}else{
smallNums[smallIndex] = nums[i];
smallIndex++;
}
}
//左邊和右邊再各自使用遞歸調用
smallNums = quickSort(smallNums);
bigNums = quickSort(bigNums);
//而後再把smallNums的全部數值賦給data左邊,而後nums中間爲data,而後再bigNums把data右邊。
for (int i = 0; i < smallNums.length; i++) {
nums[i] = smallNums[i];
}
nums[smallNums.length] = data;
for (int i = smallSize + 1; i < bigNums.length + smallSize + 1; i++) {
nums[i] = bigNums[i - smallSize - 1];
}
複製代碼
固然這也是能夠實現的,但是感受代碼不少,並且每次調用quickSort進行遞歸的時候,都要新建二個數組,這樣後面遞歸調用次數越多,新建的數組對象也會不少。咱們可不能夠思路不變,參考值左邊是小的值,參考值右邊是大的值,可是不新建數組。答案是固然!!(這逼裝的太累了,休息一下。)
剩下的左邊和右邊的數組也都經過遞歸執行這個方法便可。
public static void QuickSort(int[] nums, int start, int end) {
//若是start >= end了,說明數組就一個數了。不須要排序
if(start >= end){
return;
}
//取第一個數爲參考值
int data = nums[start];
int i = start, j = end;
//當 i 和 j 尚未碰到一塊兒時候,一直重複移動 j 和 i 等操做
while (i != j) {
//當 j 位置比參考值大的時候,繼續往左邊移動,直到找到一個比參考值小的數才停下
while (nums[j] >= data && i < j) {
j--;
}
//當 i 位置比參考值小的時候,繼續往右邊移動,直到找到一個比參考值大的數才停下
while (nums[i] <= data && i < j) {
i++;
}
//交換二邊的數
if (i < j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
//當退出上面的循環,說明 i 和 j 的位置交匯了,更換參考值與 i 位置的值。
nums[start] = nums[i];
nums[i] = data;
//左邊的數組經過遞歸繼續調用,這時候由於參考值在 i 的位置,因此左邊是從start 到 i -1
QuickSort(nums, start, i - 1);
//右邊的數組經過遞歸繼續調用,這時候由於參考值在 i 的位置,因此右邊是從 i -1 到 end
QuickSort(nums, i + 1, end);
}
複製代碼
能夠看到,咱們是默認把第一個數字當作是排好序的數組(廢話,一個數字固然是排好序的),而後每次後一個跟前面的進行比較排序,而後重複。因此咱們能夠看到最外面的一共是N-1層。而後每一層裏面的比較次數是跟當前層數數量相同。
好比第二層。咱們的數字2要和前面的1,3比較,那就要先跟3比較**(固然若是此處比3大就不須要比較了,由於前面已是個有序數組了,你比這個有序數組最大的值都大,前面的就不須要比了。)**若是比3小就要跟1比較,正比如較2次,跟層數相同。
因此基本代碼確定是:
for(int i = 1; i < n ; i ++){
if( 當前待比較的數 >= 前面的有序數組最後一個數){
continue; //這就不必比較了。
}
for(int j = i-1 ; j >=0 ; j--){
// 當前待比較數 與 前面的有序數組中的數一個個進行比較。而後插在合適的位置。
}
}
複製代碼
針對這個排序,代碼原本只須要像下面這個同樣便可:
public static void InsertSort(int[] nums) {
for (int i = 1; i < nums.length; i += 1) {
if (nums[i - 1] <= nums[i]) {
continue;
}
int va = i;
int data = nums[i];
for (int j = i - 1; j >= 0; j--) {
if (nums[j] > data) {
va = j;
nums[j + 1] = nums[j];
}
}
nums[va] = data;
}
}
複製代碼
由於咱們這裏的數字是連續的,因此間隔是1,可是爲了下一個排序的講解方便,咱們假設它們的間隔是可能不是1,因此改形成下面這個:
public static void InsertSort(int[] nums, int gap) {
for (int i = gap; i < nums.length; i += gap) {
if (nums[i - gap] > nums[i]) {
int va = i;
int data = nums[i];
for (int j = i - gap; j >= 0; j -= gap) {
if (nums[j] > data) {
va = j;
nums[j + gap] = nums[j];
}
}
nums[va] = data;
}
}
}
複製代碼
希爾排序是直接插入排序算法的一種更高效的改進版本。
咱們假設如今是1-6個數字,咱們取數組的<數量/2>爲間隔數(ps:因此爲6/2 = 3),而後按照這個間隔數分別分組:
這樣咱們能夠當場有三組數組{3,4,},{1,6},{5,2} 而後對每組數組使用直接插入排序。而後咱們把間隔數再除以2(PS:爲 3/2 = 1,取爲1)。
而後再使用直接插入排序就能夠獲得最後的結果了。
因此還記不記得咱們上面的直接插入排序代碼寫成了public static void InsertSort(int[] nums, int gap)
,就是爲了考慮上面的多個間隔不爲1的數組。
因此只要考慮咱們的循環了幾回,每次間隔數是多少就能夠了:
public void ShellSort(int[] nums) {
int length = nums.length;
for (int gap = length / 2; gap > 0; gap /= 2) {
InsertSort(nums, gap);
}
}
複製代碼
是否是發現超級簡單。
(ps:這裏記錄一下,好比有10個數字,由於理論上是每次除以2,好比應該是5,2,1; 可是有些文章是寫着5,3,1,有些文章寫着5,2,1。我寫的代碼也是5,2,1。。。o(╥﹏╥)o到底哪一個更準確點。)
選擇排序很簡單,就是每次遍歷,找出最小的一個放在前面(或者最大的一個放在後面),而後接着把剩下的再遍歷一個個的找出來排序。
public void selectSort(int[] nums) {
int min;
int va;
for (int i = 0; i < nums.length; i++) {
min = nums[i];
va = i;
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < min) {
min = nums[j];
va = j;
}
}
int temp = nums[i];
nums[i] = min;
nums[va] = temp;
}
}
複製代碼
這裏我暫時空着,由於跟二叉樹有關係,因此我準備先寫一篇二叉樹的數據結構,而後再寫這個排序。有興趣的能夠本身去搜下。
歸併算法,指的是將兩個順序序列合併成一個順序序列的方法。
好比有數列{6,202,100,301,38,8,1} 初始狀態:6,202,100,301,38,8,1 第一次歸併後:{6,202},{100,301},{8,38},{1},比較次數:3; 第二次歸併後:{6,100,202,301},{1,8,38},比較次數:4; 第三次歸併後:{1,6,8,38,100,202,301},比較次數:4;
這個引入網絡上的圖片了:
根據這個咱們能夠看到,咱們要先不停的取中間拆分,左右二邊拆開,一直拆到爲一個元素的時候中止,而後再合併。
public void mergeSort(int[] nums, int L, int R) {
//若是隻有一個元素,那就不用排序了
if (L == R) {
return;
} else {
int M = (R + L) / 2;//每次取中間值
mergeSort(nums, L, M);//經過遞歸再把左邊的按照這個方式繼續不停的左右拆分
mergeSort(nums, M + 1, R);//經過遞歸再把右邊的按照這個方式繼續不停的左右拆分
merge(nums, L, M + 1, R);//合併拆分的部分
}
}
複製代碼
咱們繼續經過圖片來講明上圖最後合併的操做:
而後重複這個操做。若是好比左邊的都比較完了,右邊還剩好幾個,只須要把右邊剩下的所有都移入便可。
public void merge(int[] nums, int L, int M, int R) {
int[] leftNums = new int[M - L];
int[] rightNums = new int[R - M + 1];
for (int i = L; i < M; i++) {
leftNums[i - L] = nums[i];
}
for (int i = M; i <= R; i++) {
rightNums[i - M] = nums[i];
}
int i = 0;
int j = 0;
int k = L;
//左邊尚未所有比較完,右邊尚未所有比較完
while (i < leftNums.length && j < rightNums.length) {
if (leftNums[i] >= rightNums[j]) {
nums[k] = rightNums[j];
j++;
k++;
} else {
nums[k] = leftNums[i];
i++;
k++;
}
}
//二邊的比完以後,若是左邊還有剩下,就把左邊的所有移入數組尾部
while (i < leftNums.length) {
nums[k] = leftNums[i];
i++;
k++;
}
//二邊的比完以後,若是右邊還有剩下,就把右邊的所有移入數組尾部
while (j < rightNums.length) {
nums[k] = rightNums[j];
j++;
k++;
}
}
複製代碼
先說明一個簡單的桶排序吧:
好比咱們要給{5,3,5,2,8}排序,咱們初始化一組內容爲0的數組(作爲桶),只要把他們當作數組的index值,好比第一個是5,咱們就nums[5] ++ ; 這樣咱們只要對這個數組遍歷,取出裏面的值,只要不爲0就打印出來。
可是這樣這裏就會有一個問題了,就是若是個人數組裏面最大的數是100000,那豈不是我初始化的數組長度是100000了,明顯不能這樣。咱們知道一個數字確定是由{0-9}這些數組成,只是處於不一樣的位數而已,因此咱們能夠仍是按照{0-9}來放入某個桶,可是是先按照個位數排序,而後按照十位數,百位數,千位數.....等來同樣樣來放。
因此咱們從左到右,從上到下的新數組的順序是{1000,25,8,158}
因此新數組是{1000,8,25,158}
3.第三次,咱們比較百位數:
因此新數組仍是{1000,8,25,158}
這時候咱們就能夠看到最終排序是{8,25,158,1000}
ps:若是還有萬位數等,持續進行以上的動做直至最高位數爲止
咱們既然知道了,要一直最外層循環要進行最高數的次數,因此咱們第一步是找出最大的數有幾位:
//能夠經過遞歸找最大值:
public static int findMax(int[] arrays, int L, int R) {
//若是該數組只有一個數,那麼最大的就是該數組第一個值了
if (L == R) {
return arrays[L];
} else {
int a = arrays[L];
int b = findMax(arrays, L + 1, R);//找出總體的最大值
if (a > b) {
return a;
} else {
return b;
}
}
}
//也能夠經過for循環找最大值:
public static int findMax(int[] arrays, int L, int R) {
int length = arrays.length;
int max = arrays[0];
for (int i = 1; i < length; i++) {
if (arrays[i] > max) {
max = arrays[i];
}
}
return max;
}
複製代碼
而後主要的排序代碼:
public static void radixSort(int[] arrays) {
int max = findMax(arrays, 0, arrays.length - 1);
//須要遍歷的次數由數組最大值的位數來決定
for (int i = 1; max / i > 0; i = i * 10) {
int[][] buckets = new int[arrays.length][10];
//獲取每一位數字(個、10、百、千位...分配到桶子裏)
for (int j = 0; j < arrays.length; j++) {
int num = (arrays[j] / i) % 10;
//將其放入桶子裏
buckets[j][num] = arrays[j];
}
//回收桶子裏的元素
int k = 0;
//有10個桶子
for (int j = 0; j < 10; j++) {
//對每一個桶子裏的元素進行回收
for (int l = 0; l < arrays.length; l++) {
//若是桶子裏面有元素就回收(數據初始化會爲0)
if (buckets[l][j] != 0) {
arrays[k++] = buckets[l][j];
}
}
}
}
}
複製代碼
通常來講外排序分爲兩個步驟:預處理和合並排序。首先,根據可用內存的大小,將外存上含有n個紀錄的文件分紅若干長度爲t的子文件(或段);其次,利用內部排序的方法,對每一個子文件的t個紀錄進行內部排序。這些通過排序的子文件(段)一般稱爲順串(run),順串生成後即將其寫入外存。這樣在外存上就獲得了m個順串(m=[n/t])。最後,對這些順串進行歸併,使順串的長度逐漸增大,直到全部的待排序的記錄成爲一個順串爲止。
最後附上百度上的排序圖:
文章哪裏不對,幫忙指出,謝謝。。o( ̄︶ ̄)o
參考:
《大話數據結構》
《算法圖解》
《啊哈,算法》