项目介绍
- 本项目通过分解各大厂的常见笔面试题,追本溯源至数据结构和算法的底层实现原理,知其然知其所以然;
- 建立知识结构体系,方便查找,欢迎更多志同道合的朋友加入项目AlgorithmPractice,(欢迎提issue和pull request)。
Part One : 内部排序:
- 1、冒泡排序
- 2、堆排序
- 3、插入排序
- 4、选择排序
- 5、桶排序
- 6、计数排序
- 7、归并排序
- 8、基数排序
- 9、希尔排序
- 10、快排及其改进
- 10.1、更详尽版:归并、桶排、基数、计数、希尔排序的具体Java实现
Part Two : 内部排序的应用:
- 11.1、解压字符串并且排序输出
- 11.2、找出超过半数的那个数字
- 11.3、返回数组中最小的k个数
- 11.4、返回数组中最小的第k个数
- 11.5、统计字符串中首先出现三次的英文字符
正文开始
1、冒泡排序
- 代码实现:BubbleSort
- 设计思路:
- 什么是冒泡排序:
- 1.每次比较相邻的两个元素
- 2.每次从头比到尾,能确定一个元素的最终位置
- 3.除了之前最终确定的元素不需要比对外,其他元素重复1和2步骤。
- 冒泡排序的时间复杂度:
- O( )。
- (在一般情况下)第一次对比n-1次,第二次对比n-2次,…,第n-1次对比1次,共对比(1+n-1)*n-1=O( )。
- 冒泡排序的稳定性:稳定。
- 【代码展示】
- 什么是冒泡排序:
public class BubbleSort implements SortInterface {
//冒泡排序 参数:int类型数组,对数组进行排序
public int[] sortMethod(int[] sortNum) {
//交换变量:temp
int temp;
//当某次内部排序无数据交换发送时,flag不变,则此时数组有序,无序再循环对比
boolean flag = false;
//外部循环:循环元素个数减一次,因为最后一次默认有序
for (int i = 0; i < sortNum.length - 1; i++) {
//内部循环:外部循环i次,可以确定i个元素已经有序,因此剩余n-i个无序元素,
//内部循环次数为元素个数n减去外部循环次数i,又因为i和i+1对比,因此防止的对比越界,需再减一
for (int j = 0; j < sortNum.length - i - 1; j++) {
if (sortNum[j] > sortNum[j + 1]) {
temp = sortNum[j];
sortNum[j] = sortNum[j + 1];
sortNum[j + 1] = temp;
flag = true;
}
}
if (!flag) {
break;
}
flag = false;
}
return sortNum;
}
}
- 注意事项:内外层循环需要注意区分范围。
2、堆排序
- 代码实现:HeapSort,其实堆排序有两种实现方式,对比看看能不能更深入理解一下,堆排序的两种写法。
- 设计思路:
- 什么是堆排序:
- 初试化建堆,建完后,堆顶即最大/最小元素。
- 交换堆顶和数组末尾元素,然后针对剩余的n-1个元素,对堆顶元素进行调整即可。
- 重复2),直到所有元素有序。
- 堆排序的时间复杂度:
- 综合:O(n log 2n)。
- 整堆的时间复杂度:O(n log 2n)。
- 建堆的时间复杂度:O(n/2 * log 2n)。
- 堆排序的稳定性:不稳定。
- 【代码展示】(整堆的代码请见github)
- 什么是堆排序:
public int[] sortMethod(int[] heap) {
int temp;
//输入检查
if (heap == null || heap.length <= 1) {
return heap;
}
//初试化建堆
for (int i = (heap.length - 1) / 2; i >= 0 ; i--) {
heapify_big(heap, i, heap.length - 1);
}
//交换堆顶和数组末尾元素,循环整堆,注意边界值
for (int i = heap.length - 1; i > 0; i--) {
temp = heap[0];
heap[0] = heap[i];
heap[i] = temp;
heapify_big(heap, 0, i-1);
}
return heap;
}
- 注意事项:堆的边界不明确,会导致数组越界错误。
3、插入排序
- 代码实现:InsertSort
- 设计思路:
- 什么是插入排序:
- 将待排序区划分为有序和无序区,初始时有序区只有一个数,即自身有序。
- 从无序区选取数,按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的纪录插入完为止。
- 插入排序的时间复杂度:
- O(n)。
- (在一般情况下)第一个元素自身有序,第二个元素跟第一个对比,第三个元素跟前两个元素对比。。。共对比(0+1+2+。。。+(n-1))=O(n)。
- 插入排序的稳定性:稳定。
- 【代码展示】(指定范围的插入排序请见github)
- 什么是插入排序:
public int[] sortMethod(int[] sortNum) {
int temp = 0;
//外部循环从1开始,直到最后
for (int i = 1; i < sortNum.length; i++) {
//内部循环从外部位置遍历到0
//====实现1
int j = i;
temp = sortNum[i];
while ((j > 0) && (temp < sortNum[j - 1])) {
sortNum[j] = sortNum[j - 1];
j--;
}
if (j != i) {
//因为在循环内j已经被减过。所以此处赋值给j,而不是j-1
sortNum[j] = temp;
}
//====实现2
/*for (int j = i; j > 0; j--) {
if(sortNum[j] < sortNum[j - 1]){
temp = sortNum[j];
sortNum[j] = sortNum[j - 1];
sortNum[j - 1] = temp;
}
}*/
}
return sortNum;
}
- 注意事项:插入点的选择
4、选择排序
- 代码实现:SelectSort
- 设计思路:
- 什么是选择排序:
- 将待排序区划分为有序和无序区,有序区初始值为0。
- 每次从待排序的数据元素序列中选出最小(或最大)的一个元素,
- 存放在序列的起始位置,直到全部待排序的数据元素排完。
- 选择排序的时间复杂度:
- O( )。
- (在一般情况下)每次选出一个最大或者最小,那么n个元素需要选择n-1次(外部排序趟数n-1),前 i 趟确定 i 个元素的最终位置(跟冒泡一样),每趟排序的对比次数为 n-i。
- 选择排序的稳定性:
- 选择排序是不稳定的排序方法。
- (比如序列[5, 5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面)。
- 【代码展示】
- 什么是选择排序:
方法一:
for(int i=0;i<A.length-1;i++){//前i趟确定i个元素的最终位置
for(int j=i+1;j<A.length;j++){//每趟排序从i开始,到A.length结束
if(A[i]>A[j]){
int temp=A[j];//交换元素
A[j]=A[i];
A[i]=temp;
}
}
}
方法二:
for(int i=0;i<A.length-1;i++){//前i趟确定i个元素的最终位置
int flag=i;//标记元素
for(int j=i+1;j<A.length;j++){//每趟排序从i开始,到A.length结束
if(A[flag]>A[j]){//如果存在元素比标记元素小,将其序号给标记
flag=j;
}
}
if(flag!=i){//如果本轮循环的最小元素不是i
int temp=A[flag];//交换元素
A[flag]=A[i];
A[i]=temp;
}
}
- 注意事项:内外层循环的范围以及交换数值的储存方式。
5、桶排序
- 代码实现:BucketSort
- 设计思路:
- 什么是桶排序:
- 首先一次遍历获取数组的最大值和最小值,从而得到桶的数量,并新建数据结构。
- 再次遍历数组,先判断属于哪个桶,再判断桶内的链表位置。
- 将桶内数据整理为数组并返回。
- 桶排序的时间复杂度:
- 第一次遍历获取桶数量: O(n)
- 找插入点:O(m + n)
- 桶排序的稳定性:桶排序是稳定的排序方法。
- 【代码展示】
- 什么是桶排序:
public int[] bucketsort(int[] array, int bucketsize) {
//创建桶,注意对象数组初始化一定要创建对象,否则为空
HashTableNode[] hashTableNodes = new HashTableNode[bucketsize];
for (int i = 0; i < bucketsize; i++) {
hashTableNodes[i] = new HashTableNode();
}
//找出最大最小值(设置array[0]存数组最大值,array[1]存最小值),并确定桶的间距
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i < array.length; i++) {
max = max > array[i] ? max : array[i];
min = min < array[i] ? min : array[i];
}
//桶的间距: 因为除法只保留商的原因,需要加1,向上取整
int bucketgap = (max - min) / bucketsize + 1;
//哈希数据(哈希之后使用插入排序,对桶内进行排序)
for (int i : array) {
int head = (i - min) / bucketgap;
//桶内插入排序
//新建节点
HashTableNode point = new HashTableNode(i);
//插入链表,注意新建的节点value均为Integer.MIN_VALUE
if (Integer.MIN_VALUE == hashTableNodes[head].value) {
hashTableNodes[head] = point;
} else {
//链表的插入排序
HashTableNode pro = hashTableNodes[head];
HashTableNode p = pro;
while (p != null && p.value < point.value) {
pro = p;
p = p.next;
}
//此处有个头节点的问题
if (hashTableNodes[head] == pro && point.value < pro.value) {
point.next = pro;
hashTableNodes[head] = point;
} else {
point.next = pro.next;
pro.next = point;
}
}
}
//整理为数组并返回
int count = 0;
for (int i = 0; i < bucketsize; i++) {
HashTableNode p = hashTableNodes[i];
while (p != null && p.value != Integer.MIN_VALUE) {
array[count] = p.value;
p = p.next;
count++;
}
}
return array;
}
- 注意事项:
6、计数排序
- 代码实现:CountSort
- 设计思路:
- 什么是计数排序:
- 是一种牺牲空间换取时间的做法,
- 首先找到数值区间,然后根据每个数对应的位置加一。
- 计数排序的时间复杂度:O(n+k)
- 计数排序的稳定性:稳定
- 【代码展示】
- 什么是计数排序:
public int[] sortMethod(int[] arrayA) {
//安全性检测
if (arrayA == null || arrayA.length <= 1) {
return arrayA;
}
//1、找出原始数组的最大最小值
int max = arrayA[0];
int min = arrayA[0];
for(int i: arrayA){
max = max > i ? max : i;
min = min < i ? min : i;
}
//2、定义新的数组大小 min到max之间,闭区间,有(max-min+1)个数字
int[] arrayB = new int[max - min + 1];
//3、一次循环统计数据——优化掉最小值之前的无用存储
for (int value : arrayA) {
arrayB[value - min]++;
}
/*for (int i = 0; i < arrayA.length; i++) {
arrayB[arrayA[i]-min]++;
}*/
//4、返回新数组
int[] arrayC = new int[arrayA.length];
//优化前方案——两次循环,需判空,浪费循环次数
/*int count = 0;
for (int i = 0; i < arrayB.length; i++) {
while(arrayB[i] != 0){
arrayC[count++] = i + min;
arrayB[i]--;
}
}*/
/*优化后方案:1、找出对应位有多少累加的统计数据;
2、根据累积数据填充b数组。
*注意事项:1、i必须到0,否则arrayA[0]无法遍历到
2、b数组,必须先--,考虑到arrayB中最小的统计位是1,表示有一个这样的数
这个数对应整个数组的最小值(即0位),所以必须先--*/
for (int i = 1; i < arrayB.length; i++) {
arrayB[i] += arrayB[i-1];
}
for (int i = arrayA.length - 1; i >= 0; i--) {
arrayC[--arrayB[arrayA[i] - min]] = arrayA[i];
}
//原址排序
for (int i = 0; i < arrayA.length; i++) {
arrayA[i] = arrayC[i];
}
return arrayA;
}
- 注意事项:
7、归并排序
- 代码实现:MergeSort
- 设计思路:
- 什么是归并排序:
- 把两个数组合并为一个数组,
- 合并的过程中,按照大小排序,那么新数组自然是有序的。
- 归并排序的时间复杂度:
- O(n log 2n)
- 时间复杂度的推导过程:归并排序时间复杂度推导
- 归并排序的稳定性:
- 【代码展示】
- 什么是归并排序:
public int[] mergesort(int[] array, int left, int right, int[] temp){
if(left < right){
int mid = (left+right)/2;
mergesort(array, left, mid, temp);
mergesort(array, mid+1, right, temp);
sort(array, left, mid, right, temp);
}
return array;
}
//闭区间合并
public void sort(int[] array, int left, int mid, int right, int[] temp) {
int l = left;
int r = mid + 1;
int j = left;
while(l <= mid && r <= right){
if(array[l] > array[r]){
temp[j++] = array[r++];
}else{
temp[j++] = array[l++];
}
}
while(l <= mid){
temp[j++] = array[l++];
}
while(r <= right){
temp[j++] = array[r++];
}
//灌装数组
for (int i = left; i <= right; i++) {
array[i] = temp[i];
}
}
- 注意事项:
8、基数排序
- 代码实现:RadixSort
- 设计思路:
- 什么是基数排序:
- 基数排序的时间复杂度:
- 基数排序的稳定性:
- 【代码展示】
public int[] sortMethod(int[] array) {
//边界判空
if (array == null || array.length <= 1) {
return array;
}
//找出最大位数
int max = Integer.MIN_VALUE;
int temp = 0;
for (int i = 0; i < array.length; i++) {
temp = array[i] > 0 ? array[i] : -array[i];
if(max < temp){
max = temp;
}
}
int d = String.valueOf(max).length();
//基数排序位数d 和 radix
radixsort(array, d, 10);
return array;
}
public int[] radixsort(int[] array, int d, int radix) {
//当前循环到的位数,从第一位到第 d 位
int radixtemp = 1;
//二维数组用于基数排序、radixpoint是二维桶内指针,每个桶一个,所以其本身也是一个数组
int[][] radixbucket = new int[radix][array.length];
int[] radixpoint = new int[radix];
int temp = 0;
//循环控制位 loopcount
for(int loopcount = 0; loopcount <= d; loopcount++){
//基数排序(可以使用数组加链表的哈希结构或者二维数组,利弊均有)
for (int i = 0; i < array.length; i++) {
//取出当前尾数,用于排序
temp = (array[i] / radixtemp) % radix;
radixbucket[temp][radixpoint[temp]] = array[i];
radixpoint[temp]++;
}
//基数统计合并,通过桶内指针判断桶内是否还有元素
int bucketcount = 0;
for (int i = 0; i < radix; i++) {
int j = 0;
while(radixpoint[i] != 0){
array[bucketcount++] = radixbucket[i][j++];
radixpoint[i]--;
}
}
//修改循环变量
radixtemp *= radix;
}
return array;
}
- 注意事项:
9、希尔排序
- 代码实现:ShellSort
- 设计思路:
- 什么是希尔排序:
- 将原本的一个数组,分成若干个数组,独自排序
- 第一次分成n个数组,第二次分成n/2个数组,第三次分成n/4个…
- 每次排序才有插入排序来调整序列
- 希尔排序的时间复杂度:
- 希尔排序的稳定性:
- 不稳定,因为分队排序的时候,可能会导致排序的先后顺序改变
- 【代码展示】
- 什么是希尔排序:
public class ShellSort implements SortInterface {
@Override
public void sortMethod(int[] array) {
int length = array.length;
//间隔gap ,直到gap为1结束
int gap = length / 2;
for (; gap >= 1; gap /= 2) {//注意此处的gap等于1,就相当于全排序,是需要的,“=”不能少
for (int i = 0; i < gap; i++) {
//内部使用插入排序
for (int j = i + gap; j < length; j += gap) {
int temp = array[j];
int k = j;
while ((k - gap) >= 0 && (array[k - gap] > temp)) {//k-gap 要判断越界
array[k] = array[k - gap];
k -= gap;
}
if (k != j) {
array[k] = temp;
}
}
}
}
}
}
- 注意事项:
- gap的大小设置,
- 注意到代码中含有四次嵌套的循环,每次的起止需要注意
- 注意一下越界问题
- 插入排序的写法需要注意
10、快排及其改进
这篇文章针对快排的单项、双向、改进分别做了详细解释,
请移步:快排及其优化
扫描二维码关注公众号,回复:
11686620 查看本文章
11.1、解压字符串并且排序输出
- 代码实现:Decompress
- 设计思路:
- 解题步骤
- 代码展示
在这里插入代码片
- 注意事项:
11.2、找出超过半数的那个数字
- 代码实现:HalfPastNum
- 设计思路:
- 解题步骤
- 代码展示
在这里插入代码片
- 注意事项:
11.3、返回数组中最小的k个数
- 代码实现:ReturnKMin
- 设计思路:
- 解题步骤
- 代码展示
在这里插入代码片
- 注意事项:
11.4、返回数组中最小的第k个数
- 代码实现:ReturnKthMin
- 设计思路:
- 解题步骤
- 代码展示
在这里插入代码片
- 注意事项:
11.5、统计字符串中首先出现三次的英文字符
- 代码实现:StatisticFirstThreeChar
- 设计思路:
- 解题步骤
- 代码展示
在这里插入代码片
- 注意事项: