最近因为找实习和为几个月后的工作笔试与面试做准备,不得不把很久没有碰过的一些面试笔试常用的基础算法和数据结构拿来复习一遍,想起来那本带我入门的Algorithms好像也被我放在书架底下吃灰了,说来惭愧,好了,进入正题吧!!
1. 选择排序
我们这里是按照升序的顺序进行原理说明和代码实现。
首先,我们来说明一种最简单的排序算法——选择排序算法。原理是:首先,找到数组中最小的那个元素,之后将它和数组中的第一个元素交换位置,其次再在剩余的元素中找到最小的元素,将它与数组中的第二个元素交换位置,如此循环往复,直到整个数组排序。选择排序这个名称的由来就是因为它在不断地选择剩余元素之中的最小者。
特点:运行时间和输入无关,有序序列和无序序列所用的排序时间是一样长的;数据移动是所有排序里面最少的,选择排序只进行了N次交换,这里N是数组元素的个数,它交换次数和数组大小是线性的关系,其他大部分排序算法交换次数的增长量级都是线性对数或是平方级别的。
//先写数组中元素交换的方法
private static void swap(int[] arr, int i, int j) {
int temp;
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
选择排序具体的实现代码如下:
private static void selectSort(int[] arr) {
for(int i = 0; i < arr.length - 1; i++) {
for(int j = i + 1; j < arr.length; j++) {
if(arr[i] > arr[j])
swap(arr, i, j);
}
}
}
- 复杂度分析:选择排序算法平均时间复杂度和最差时间复杂度都是O(n^2),因此它是一个稳定的算法;空间复杂度为O(1)
2. 直接插入排序
我们需要将一个元素插入到其他已经有序的序列之中的适当位置。为了给插入的元素腾出空间,需要将其余所有元素在插入之前都向右移动一位。插入排序和选择排序一样,当前索引左边的所有元素都是有序的,但左边元素的最终位置不确定,为了给更小的元素腾出空间,他们可能会被移动,但是索引如果到达了数组最右端,排序就完成了。
特点:插入排序所用时间取决于输入中元素的初始顺序,例如一个有序的或是接近有序的数组排序会比完全无序的数组排序快得多。
实现代码:
private static void insertSort(int[] arr) {
for(int i = 1; i < arr.length; i++) {
for(int j = i; j > 0; j--) {
if(arr[j - 1] > arr[j])
swap(arr, j-1, j);
}
}
}
- 复杂度分析:插入排序算法平均时间复杂度和最差时间复杂度都是O(n^2),它也是一个稳定的算法;空间复杂度为O(1)
3. 希尔排序
直接插入排序处理大规模乱序数组时速度会很慢,因为它只会交换相邻的元素,元素只能一位一位地从数组一端移动到数组的另一端,希尔排序为了加快速度改进了插入排序,交换不相邻的元素对数组局部进行排序,并最终用插入排序将局部有序的数组进行排序。希尔排序最主要的思想就是使数组中任意间隔为h的元素都是有序的,我们将这样的数组序列称为h有序数组。实现希尔排序的一种简单方法就是在h子数组中将每个元素交换到比它大的元素之前,将插入排序中移动元素的距离由1改为h即可,这样就将希尔排序转化为类似于插入排序但使用不同增量的过程。
特点:希尔排序可以说是对插入排序的改进方法,它更高效的原因是权衡了子数组的规模和有序性。排序之时,子数组很短,排序结束之时各个子数组都是部分有序的,这两种情况适合插入排序。
代码实现:
private static void shellSort(int[] arr) {
int len = arr.length;
int h = 1;
while(h < len/3) {
h = h*3 + 1;
}
while(h >= 1) {
//外循环控制增量大小,每循环一次增量进行递减操作,直至增量为1,即最后一次为插入排序
for(int i = h; i < arr.length; i++) {
for(int j = i; j >= h; j -= h) {
if(arr[j - h] > arr[j])
swap(arr, j, j-h);
}
}
h /= 3;
}
}
- 复杂度分析:希尔排序的时间复杂度的计算比较复杂,这里直接给出结论。在最优的情况下,时间复杂度为:O(n ^ (1.3) ) (元素已经排序好顺序),在最差的情况下,时间复杂度为:O(n ^ 2);希尔排序是非稳定的算法。
冒泡排序从数组的左端开始索引,在相邻元素之间比较大小,将值较大的交换到右边,这样进行完一次后就将最大的元素的值交换到了数组的最右边,就好像气泡从水底一直上升到水面变的最大,这也就是“冒泡排序”的由来。这样循环进行“冒泡”,当索引到达了最右边,则排序完成。
特点:对每一对相邻元素做相同的工作,一直从第一对到数组结尾的最后一对。
代码实现:
private static void bubleSort(int[] arr) {
for(int i = 0; i < arr.length - 1; i++) {
for(int j = 0; j < arr.length - 1 - i; j++) {
if(arr[j] > arr[j+1])
swap(arr, j, j + 1);
}
}
}
- 复杂度分析:冒泡排序平均时间复杂度为:O(n ^ 2),最坏情况为:O(n ^ 2),最好情况为:O(n );冒泡排序是稳定的排序算法。
快速排序采用分冶法的思想,对一个数值序列进行划分,划分为两个子序列,然后对两个子序列进行分冶法的思想。具体的实现原理是从数值队列中选择一个基准点元素,将队列中的其他元素与基准元素比较,比基准元素小的放在基准元素左边,大的放在基准元素右边,此时基准元素划分序列为左右两个子序列,对左右子序列进行递归划分,则最终形成有序序列。
特点:快速排序实现简单,适用于不同输入数据,是原地排序,对长度为N的数组排序所需时间是线性对数的增长量级。快速排序的缺点是,快速排序非常脆弱,实现时的错误可能导致快排的性能大大降低。
代码实现:
//快速排序主方法,这里功能主要实现递归
private static void quickSort(int[] arr, int left, int right) {
if(left < right) {
int dp = partition(arr, left, right);
quickSort(arr, dp + 1, right);
quickSort(arr, left, dp - 1);
}
}
//基准点选择方法,指针移动
private static int partition(int[] arr, int left, int right) {
//存放初始的基准点,因为中间要不断的赋值所以存放起来
int keys = arr[left];
while(left < right) {
//从右边如果大于基准点,则直接指针向左
while(arr[right] >= keys && left < right) {
right--;
}
//否则,将右指针的值赋给左指针,这里实现了将大于基准点的元素交换到基准点左边
arr[left] = arr[right];
//从右边开始,若左指针小于基准,则指针向右
while(arr[left] <= keys && left < right) {
left++;
}
//否则,将左指针的值赋给右指针
arr[right] = arr[left];
}
//最后若左右指针相遇,则将基准值赋给左指针值,并返回索引号作为分割点
arr[left] = keys;
return left;
}
- 复杂度分析:快速排序平均时间复杂度为:O(nlog2n),最坏情况为:O(n ^ 2),最好情况为:O(nlog2n);快速排序是非稳定的排序算法。
归并排序是指将两个已经有序的序列合并成一个有序序列的排序算法,归并排序的原理是:首先,申请存放合并后数值序列的存放空间,空间大小为数值序列A,B的空间之和;初始化两个指针指向两个序列的首元素地址,比较两个指针对应元素的值,将较小值放入最终存放空间,并移动较小值指针指向序列下一位置,重复前一步骤,直到某个指针指向序列队尾,没有剩余元素和另一个序列比较,则直接将另一序列剩余元素复制到最终序列存放空间末尾。
特点:保证排序数量为N的数组所需时间和N的线性对数成正比,主要缺点是所需额外的存储空间和N成正比。
代码实现:
private static void mergeSort(int[] arr, int left, int right) {
//这是递归分冶的方法
if(left < right) {
int mid = (left + right) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
//这里是归并的方法
private static void merge(int[] arr, int left, int mid, int right) {
int i = left;
int j = mid + 1;
//缓存数组
int[] temp = new int[arr.length];
for(int k = 0; k < temp.length; k++) {
temp[k] = arr[k];
}
for(int k = left; k <= right; k++) {
//当左子序列长度为0,直接将右子序列剩余元素赋给最终序列队尾
if(i > mid)
arr[k] = temp[j++];
//当右子序列长度为0,直接将左子序列剩余元素赋给最终序列队尾
else if(j > right)
arr[k] = temp[i++];
else if(temp[i] > temp[j])
arr[k] = temp[j++];
else arr[k] = temp[i++];
}
}
- 复杂度分析:归并排序平均时间复杂度、最最好均为:O(nlog2n);归并排序是稳定的排序算法。
堆排序是基于堆的数据结构实现的排序算法。堆排序比较复杂,具体的原理可以参考这篇博客,讲的很详细也很通俗易懂。
https://www.cnblogs.com/chengxiao/p/6129630.html点击打开链接
代码实现:
public static void heapSort(int []arr){
//1.构建大顶堆,这里arr.length/2-1是为了取到最后一个非叶子结点
for(int i=arr.length/2-1;i>=0;i--){
//从最后一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr,i,arr.length);
}
//2.调整堆结构+交换堆顶元素与末尾元素
for(int j=arr.length-1;j>0;j--){
swap(arr,0,j);//将堆顶元素与末尾元素进行交换
adjustHeap(arr,0,j);//重新对堆进行调整
}
}
public static void adjustHeap(int []arr,int i,int length){
int temp = arr[i];//先取出当前元素i
for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始
if(k+1<length && arr[k]<arr[k+1]){//如果左子结点小于右子结点,k指向右子结点
k++;
}
if(arr[k] >temp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[i] = arr[k];
i = k;
}else{
break;
}
}
arr[i] = temp;//将temp值放到最终的位置
}
- 复杂度分析:堆排序平均时间复杂度、最坏、最好均为:O(nlog2n);堆排序是稳定的排序算法。
基数排序的原理是将数值按照位切分为不同数字,然后对每位数分别进行比较,从而达到排序的目的。首先,将所有需要进行比较的数值序列统一为相同的长度位数,若元素长度较短则在前面补零;从最低位开始依次进行排序,从次低位继续开始排序,重复上一步,由低向高排序,最终得到完整有序序列。
代码实现:
private static void baseSort(int[] arr) {
//获取最大的位数
int maxLen = 0;
for(int num : arr) {
if(num > maxLen)
maxLen = num;
}
maxLen = (maxLen + "").length();
int[] temp = new int[arr.length];
int k = 1;
for(int i = 0; i < maxLen; i++) {
for(int j = 0; j < arr.length; j++) {
//缓存数组是对应元素每一位的数
temp[j] = (arr[j] / k)%10;
}
k *= 10;
//每一位排序进行桶排序
bucketSort(temp, arr);
}
}
//桶排序方法
private static void bucketSort(int[] arr, int[] target) {
//创造一个二维的桶
int[][] bucket = new int[10][arr.length];
int[] targetCopy = new int[target.length];
for(int i = 0; i < arr.length; i++) {
for(int j = 0; j < bucket.length;j++) {
if(arr[i] == j) {
//将对应元素放入桶中,并标志为1
bucket[j][i] = 1;
}
}
}
int k = 0;
for(int i = 0; i < 10; i++) {
for(int j = 0; j < bucket[0].length; j++) {
if(bucket[i][j] == 1) {
//按行取出桶中元素
targetCopy[k] = target[j];
k++;
}
}
}
//最后将排序好的数组赋给目标数组
for(int i = 0; i < target.length; i++) {
target[i] = targetCopy[i];
}
}
- 复杂度分析:基数排序平最好时间复杂度为:O(k*n),k为最高位数,最坏时间复杂度为O(k*n)。