零、如何不申请存储空间交换两个数
1.异或运算:
2.加减法交换
a = a + b;
b = a - b;
a = a - b;
一、冒泡排序
算法介绍: 冒泡排序是一种非常直观的排序算法。它重复的走访要排序的数列,一次比较两个元素,如果它们顺序错误就把它们交换过来。重复该工作直到没有元素再要进行交换,也就是说该数组已经排序完成。
算法步骤:
【1】第一轮循环使用第一个元素与第二个进行比较,如果第一个元素大于第二个元素就进行交换,如果第一个元素小于第二个元素说明顺序正确,无需排序,然后使用第二个元素与第三个元素比较,还是上述的规则,直至倒数第二个元素和倒数第一个元素的顺序纠正时,第一轮循环结束,这时数组中最大的元素已经被移动到数组尾部。
【2】第二轮循环照样按照上述方式一一比较,最终将数组中第二大的元素移动到数组的倒数第二个位置,而第二轮循环不需要校正倒数第二个元素和倒数第一个元素的顺序,因为第一轮已经将数组中最大的元素转移到数组的尾部
【。。。】
【最后一轮】最后一轮是数组长度减一轮,例如,数组长度为五的数组只需比较四轮。并且在上面环节中数组最大的数组长度减二的元素顺序都被排列好了,现在只剩下,最小的两个元素进行顺序校正,该轮只需要比较一轮
规律: 以数组长度为五的数组进行举例
第一轮——比较四次
第二轮——比较三次
第三轮——比较两次
第四轮——比较一次
由上述可知,每轮的轮数加该轮比较的次数都等于数组的长度
冒泡排序演示动画
算法代码实现
public static void sort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
// 外层循环循环数组长度减一次
for (int i = 1; i < arr.length; i++) {
// 内层循环第一次循环数组长度减一次,每次减一
for (int j = 0; j < arr.length - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
二、选择排序
算法介绍: 选择排序也是一种简单直观的排序算法。将数组分为有序区和无序区,数组的左边是有序区,数组的右边是无序区,每次查询到无序区的最小元素,放置在无序区的第一个位置,然后无序区的左边界右移一位,当且只有当数组中的最小元素就是无序区第一个元素时,无需交换。这种排序是有选择的交换位置,所以叫选择排序
算法步骤:
【1】第一轮:首先数组无序区的左边界是数组的首位,即数组中所有的元素都是无序的,我们假设数组中第一个位置的元素就是当前无序区的最小元素,然后从第二个位置开始遍历数组,查询无序区的最小元素的位置,如果后续查询到最小元素还小的元素,就改变最小值索引,最后如果最小值的索引不是无序区的第一个元素的索引就交换元素,反之,说明无序区的第一个元素就是最小元素,无需交换位置,第一轮循环阶数
【2】第二轮:第二轮无序区的左边界是数组第二个元素(包含),然后同样假设无序区的第一个元素为无序区的最小元素,然后从第三个元素开始,查询无序区的最小元素,找到之后,判断是否交换,然后数组中第二小的元素就排序好了
【。。。】
【最后一轮】最后一轮无序区的左边界就变成了数组中倒数第二个元素,这时假设无序区的第一个元素为无序区的最小元素,然后寻找无序区中最小的元素,之后判断是否交换。此时,数组中倒数倒数第二小的元素就排好了,剩下倒数第一个元素自然就是最小的元素。
选择排序演示动画
算法代码实现
public void sort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
for (int j = i+1; j < arr.length; j++) {
if (arr[minIndex] > arr[j]) {
minIndex = j;
}
}
if (minIndex != i) {
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
}
三、插入排序
算法介绍: 插入排序也是分为有序区和无序区,数组的左边是有序区,数组的右边是无序区,每次使用无序区中第一个元素,然后从有序区中从右向左遍历,如果遍历到的元素大于无序区中第一个元素,就将当前遍历元素的值赋值给该元素后面一个位置,否则就查到了第一个比无序区第一个元素小的,然后将无序区的元素付给当前位置的后一个位置的元素。依次使用无序区的元素插入到有序区的适当位置,最后排序完成
算法步骤:
【1】第一轮:假设数组的有序区仅为数组头部,无序区为第二个元素到数组尾部的区域。然后使用无序区第一个元素也就是数组中第二个元素,然后在有序区中查找第一个比该元素还小的元素的索引。在一轮中只需同数组中第一个元素进行比较。如果该元素比数组第一个位置的元素还小,然后将第一个位置的元素赋值给第二个位置的元素,然后遍历前一个元素发现前面没有元素了,最后把该元素赋值给第一个位置。否则,则说明顺序正确,无需校正顺序。
【2】第二轮:无序区为第三个元素到数组尾部的区域。然后取出无序区第一个元素,继续上述操作插入到数组有序区的指定位置,完成该轮排序。
【。。。】
【最后一轮】无序区为倒数第二个元素到数组尾部的区域,拿出无序区中第一个元素,插入指定位置,排序完成
插入排序演示动画
算法代码实现
public void sort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
// 外层为循环次数
for (int i = 1; i < arr.length; i++) {
int insertEle = arr[i];
int j = i - 1;
for (; j >= 0; j--) {
if (arr[j] > insertEle) {
arr[j + 1] = arr[j];
} else {
break;
}
}
arr[j + 1] = insertEle;
}
}
四、希尔排序
算法介绍: 希尔排序,也称递减增量算法,是插入排序的一种更高效的改进版本。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对已经拍好序的数组操作时,效率高
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成若干子序列分别进行直接插入排序,待整个记录中的记录“基本有序”时,在对全体记录依次直接插入排序。
算法步骤:
【1】第一轮:先定义一个步长step(step=数组的长度除2),然后依次对数组中第i,(i从step开始)个元素和第i-step进行插入排序,直到 i 最后到达数组末尾
【2】第二轮:再将步长step除2,然后对四部分的第 i 个元素分别进行插入排序
【。。。】
【最后一轮】:最后一轮,step = 1,相当于对数组整体进行一次插入排序
插入排序演示动画
算法代码实现
public void sort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
int length = arr.length;
for (int step = length / 2; step >= 1; step /= 2) {
for (int i = step; i < length; i++) {
int insertEle = arr[i];
int j;
for (j = i - step; j >= 0; j -= step) {
if (arr[j] > insertEle) {
arr[j + step] = arr[j];
} else {
break;
}
}
arr[j + step] = insertEle;
}
}
}
插入排序和希尔排序测试
前面说到希尔排序是插入排序的优化,先对部分数据进行部分有序化,这样会提升排序效率,我不信邪,明明都是两层循环,谁能不谁强到那里去,然后我进行了性能的测试。
具体测试样例:先创建了长度为100万的数组,然后使用10001以内的元素填充数组,最后分别使用插入排序和希尔排序进行排序。不测不知道,一测吓一跳。希尔排序经过多次测试,其执行时间再0.2S到0.3S,而插入排序执行时间第一次测试124S,第二次测试143S,多的测试不了了,真的等不下去。由此可知希尔排序很强,并且在数组长度越长的时候,这种差距就越明显。
测试数据
int[] arr = new int[1000000];
Random random = new Random();
for (int i = 0; i < 1000000; i++) {
arr[i] = random.nextInt(10001);
}
InsertionSort insertionSort = new InsertionSort();
long start = System.nanoTime();
insertionSort.sort(arr);
long end = System.nanoTime();
System.out.println("消耗时间"+(end-start)/1000000000.0+"s");
System.out.println(Arrays.toString(arr));
五、归并排序
算法介绍: 归并排序是建立在归并操作上的一种有效的排序方法。该算法是采用分治法的一个非常典型的应用。
其次,归并排序是,先递归将数组拆分成一个个数组长度为一的数组,无需排序。然后我们对这些长度为一的数组,相邻的两两归并操作排序,完成之后,长度为一个的数组合并成数组长度为二的一个一个数组。然后继续对这些长度为二的数组相邻数组之间两两执行归并操作排序,最后到一开始递归的时候,执行归并操作的两个数组分别为元素待排序数组的左右两部分。然后对这两个数组进行归并操作排序,这个算法由于不断的拆分数组创建数组,以及不断的递归,在数组长度过长的情况下,效测试效率同插入排序一致,执行时间都在100秒以上
算法步骤:
【1】一开始使用递归,将数组不断拆分,直到最后待排序数组被拆分成一个个数组长度为一的数组,然后相邻数组之间两两排序,最后这些数组被合并成长度为二的一个一个数组,并且两两排序完成
【2】然后,长度为二的数组之间相邻之间两两排序,最后这些数组又被合并称为长度都为四的一个一个数组并且每组排序完成
【。。。】
【最后一轮】最后一轮归并操作回到了一开始递归的时候,要执行归并操作的两个数组分别为一开始的待排序数组的左半和右半,最后对这两个数组执行归并操作,待排序数组便排序完成
归并排序动画演示
算法代码实现:
/**
* 归并排序,分别对两个数组进行排序
*/
public int[] sort(int[] arr) {
if (arr.length == 1) {
return arr;
}
int mid = arr.length / 2;
int[] nums1 = Arrays.copyOfRange(arr, 0, mid);
int[] nums2 = Arrays.copyOfRange(arr, mid, arr.length);
return merge(sort(nums1), sort(nums2));
}
public int[] merge(int[] nums1,int[] nums2) {
// 入参判断
if (nums1 == null || nums1.length == 0) {
return nums2;
}
if (nums2 == null || nums2.length == 0) {
return nums1;
}
int length1 = nums1.length;
int length2 = nums2.length;
int[] res = new int[length1+length2];
// 定义索引分别记录当前即将被添加元素的索引
// k索引记录返回数组中的即将要添加元素的索引
int i = 0, j = 0, k = 0;
while (i < length1 && j < length2) {
// 如果数组1的当前元素小于数组2当前元素,就添加数组1当前元素,是最终数组升序排列
res[k++] = nums1[i] < nums2[j] ? nums1[i++] : nums2[j++];
}
// 到这里,说明数组1或者数组2添加完毕,还有可能有数组元素还未添加完毕
while (i < length1) {
res[k++] = nums1[i++];
}
while (j < length2) {
res[k++] = nums2[j++];
}
return res;
}
六、快速排序
算法介绍: 快速排序也采用了分治的思想进行排序。首先从数列中选择一个基准,我们这里将数组中的第一个元素作为基准数。然后在排序中找到基准点,该基准点左侧的元素比基准数小,右侧的元素总是比基准数大,然后使用递归不断对基准点左右两侧数组继续执行上述操作,最后排序完成
算法步骤:
首先选择数列首位作为基准,对数组重新排序,最后保证基准点左侧的元素比基准小,基准点右侧的元素比基准大,然后使用递归,对左右两侧的子数组重复上述操作,最终排序完成
园长的牧歌: 史上最详细图解快速排序
算法代码实现
public void sort(int[] arr, int left, int right) {
if (left >= right) {
return;
}
// 将数组中第一个元素定义为基准值
int pivot = arr[left];
// 开始寻找基准点位置
int i = left, j = right;
while (i < j) {
// 先从右边开始寻找第一个比基准值还小的位置
while (i < j && pivot < arr[j]) {
j--;
}
if (i < j) {
arr[i] = arr[j];
i++;
}
// 然后从左边开始寻找第一个比基准值还大的位置
while (i < j && pivot > arr[i]) {
i++;
}
if (i < j) {
arr[j] = arr[i];
j--;
}
}
arr[i] = pivot;
// 循环结束后,一个基准点就找到了,然后递归排序基准点左边和右边的数组
sort(arr, left, j-1);
sort(arr, j+1, right);
}
七、堆排序
堆排序是利用堆这种数据结构所设计的一种排序算法。堆中父亲节点的优先级总是高于子节点,按照优先级定义的不同又可以分为大顶堆和小顶堆:
1.大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列
2.小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列
堆排序也分为无序区和有序区,一开始,堆中所有的元素都是无序的
算法介绍: 先将数组整理成堆,然后交换堆中第一个元素与堆中最后一个元素,这样最值便移动到了堆中最后一个位置(这时,最后一个位置就是有序区),此时堆不满足堆性质了,我们在对堆中第一个元素到堆中倒数第二个元素进行下沉操作使之再次称为一个堆。然后拿到堆中第一个元素,此时该元素即为堆中第二个最值,继续同堆中无序区最后一个元素交换(这时,有序区含有排好序的两个元素),就这样重复上述操作,无序区最后消失,存储对的数组就变成了有序的数组
算法步骤:
我们这里假设对一个数组升序排序,所以排序之前理应数组已经称为一个大顶堆了
【1】交换堆中第一个元素和堆中最后一个元素,此时堆中最大的值就被交换到了堆的尾部,此时有序区就是最后一个元素形成的区域。因为交换的缘故,此时堆已不是一个大顶堆,我们对堆中第一个元素到倒数第二个元素从第一号元素开始进行下沉操作,使之再次称为一个大顶堆
【2】再次交换第一个元素和倒数第二个元素(也就是无序区第一个元素和无序区最后一个元素),此时有序区就是数组最后两个元素形成的区域。然后将无序区整理称为大顶堆
【。。】就这样一直替换无序区形成的大顶堆中第一个元素至无序区尾部,最后数组就变成了递增数组
堆排序动画演示
算法代码实现
private int getParentIndex(int index) {
if (index < 0) {
throw new RuntimeException("索引不合法");
}
return (index - 1) / 2;
}
private int getLeftIndex(int index) {
if (index < 0) {
throw new RuntimeException("索引不合法");
}
return 2 * index + 1;
}
/**
* 将数组整理成堆,需要找到最后一个节点的父节点,
* 从该父节点开始对之前的节点进行下沉操作
*
* @param arr
*/
private void heapify(int[] arr) {
int curIndex = getParentIndex(arr.length - 1);
for (; curIndex >= 0; curIndex--) {
// 对数组从curIndex到数组末尾的元素进行下沉操作
swim(arr, curIndex, arr.length);
}
}
/**
* 要整理成为最大堆,为堆排序做准备
*/
private void swim(int[] arr, int curIndex, int length) {
// 拿到下沉操作的初始位置元素的值
int val = arr[curIndex];
// 拿到左孩子节点的索引
int leftIndex = getLeftIndex(curIndex);
// 判断左孩子是否存在
while (leftIndex < length) {
// 查询左右孩子中的最大值索引,假设左孩子比右孩子元素值大
int changeIndex = leftIndex;
// 获取右孩子索引
int rightIndex = leftIndex + 1;
// 保证右孩子存在并且比较左右孩子大小,从而拿到最大值的索引
if (rightIndex < length && arr[rightIndex] > arr[leftIndex]) {
changeIndex = rightIndex;
}
// 如果当前节点小于两个孩子节点中的某个元素,就替换当前节点的值
if (arr[changeIndex] > val) {
// 将当前值修改为两孩子中的最大值
arr[curIndex] = arr[changeIndex];
// 改变当前节点为两孩子节点中的最大值的索引
curIndex = changeIndex;
// 修改左孩子索引
leftIndex = getLeftIndex(curIndex);
} else {
// 如果当前节点大于两个孩子,则该节点已下沉到了指定位置
break;
}
}
// 修改当前位置的值为val
arr[curIndex] = val;
}
public void sort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
// 整理成为最大堆
heapify(arr);
for (int i = arr.length - 1; i > 0; i--) {
// 拿出堆中最大的元素,与堆中无序区的最后一个元素进行交换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 对堆中无序区从0开始进行下沉操作
swim(arr, 0, i);
}
}
八、计数排序
算法介绍: 计数排序的核心在于将待排序数组转化为存储在额外开辟的数组空间中,开辟的数组索引表示待排序数组数组中的某个数,而该索引位置的值代码这个数出现的次数。首先将待排序数组中的信息存储在统计数组中,最后使用统计数组中的信息对待排序数组进行回填以达到排序的结果。
算法步骤:
【1】首先创建计数数组的长度,长度为待排序数组中最大值与最小值的差加一,创建完成遍历待排序数组,统计待排序数组中每个元素出现的次数(统计数组的索引是待排序数组中的元素,索引对应的值就是该元素在待排序数组中出现的次数)
【2】然后循环统计数组,将统计数组中的索引对应的元素一个个填写到待排序数组中(填写次数,统计数组中该索引对应的值次)
计数排序动画演示
算法代码实现:
public static int getMaxVal(int[] arr) {
return Arrays.stream(arr).max().getAsInt();
}
public static int getMinVal(int[] arr) {
return Arrays.stream(arr).min().getAsInt();
}
public static void sort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
// 查询数列中最大值与最小值的范围,创建数组
int max = getMaxVal(arr);
int min = getMinVal(arr);
// 创建一个长度为max-min+1的数组
int[] res = new int[max - min + 1];
// 统计数列中各元素
// 如果数组中的最小值小于0就进行偏移
if (min < 0) {
Arrays.stream(arr).forEach(item -> {
res[item + Math.abs(min)]++;
});
} else {
Arrays.stream(arr).forEach(item -> {
res[item]++;
});
}
// 进行回填
int index = 0;
// 根据最小值进行不同规则的回填
if (min < 0) {
for (int i = 0; i < res.length; i++) {
while (res[i] > 0) {
arr[index++] = i + min;
res[i]--;
}
}
} else {
for (int i = 0; i < res.length; i++) {
while (res[i] > 0) {
arr[index++] = i;
res[i]--;
}
}
}
}
算法缺点: 如果待排序数组,数据过度分散那么统计数组中的位置没有统计信息。这样就会导致空间的浪费,并且统计数组中很多位置值为0,还要耗费时间遍历,既浪费空间又浪费时间。下面使用了离散化处理数据过度分散的情况,这部分知识我还未系统学习,现在画一张图方便理解。
这样就避免了元素过度分散而导致的计数数组空间大量浪费情况
离散化处理的计数排序代码
public static void sort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
// 先将数组去重并排好序
int[] sortedSet = Arrays.stream(arr).distinct().sorted().toArray();
// 创建统计数组
int[] count = new int[sortedSet.length];
// 进行统计
Arrays.stream(arr).forEach(item -> {
// 查询数组中的元素位于数组中的位置,然后再count数组中记录
int index = find(sortedSet, item);
count[index]++;
});
// 进行回填
int index = 0;
for (int i = 0; i < sortedSet.length; i++) {
// 有的数字出现多次,需要重复填充
while (count[i] > 0) {
// 因为这里的索引是待排序数组处于sortedSet数组中的位置,
arr[index++] = sortedSet[i];
count[i]--;
}
}
}
/**
* 从去重排序数组中找到待排序数组中num的位置
*/
public static int find(int[] arr, int num) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == num) {
return mid;
} else if (arr[mid] > num) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 这里不可能存在找不到的情况,因为去重排序数组中包含待排序数组的所有元素
// 工具报错,这里只能添加一条返回结果
return -1;
}
九、桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否在于映射函数的确定。为了使得桶排序更加高效,我们需要做到这两点:
1.在额外空间充足的情况下,尽量增大桶的数量
2.使用的映射函数能将待排序数组平均的分配到每个桶中
算法介绍: 首先根据函数将不同类型的数映射到不同的桶中,然后对每个桶中的数字进行排序。最后将每个中的元素一一回填到待排序数组中
算法步骤:
情景:对每个元素处于0-99范围内的数组进行排序
【1】创建十个桶,分别存储十位数为0,1,2,3…9的元素,按照这种映射关系将待排序数组分配到每个桶中。
【2】对每个桶进行排序
【3】最后从第一个桶开始取出元素填充待排序数组,最后排序完成
桶排序示意图
桶排序代码
/**
* 假定待排序数组中元素的范围为0-99
*/
public void sort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
// 创建十个桶
List<Integer>[] buckets = new List[10];
// 初始化每个桶
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new ArrayList<>();
}
// 向桶中放入元素
Arrays.stream(arr).forEach(item -> {
// 计算该元素放置在哪个桶
int index = item / 10;
buckets[index].add(item);
});
// 使用Api对每个桶中的数据进行排序
for (int i = 0; i < buckets.length; i++) {
buckets[i].sort((o1, o2) -> o1 - o2);
}
// 进行回填
int index = 0;
for (int i = 0; i < buckets.length; i++) {
for (int j = 0; j < buckets[i].size(); j++) {
arr[index++] = buckets[i].get(j);
}
}
}
十、基数排序
算法介绍: 基数排序将数字根据位数分割成不同的数字,然后按每位数分别比较。基数排序的方式可以采用LSD或MSD,LSD的排序方式从数字的个位开始,而MSD从数组中最大元素的最高位开始。
算法步骤:
【1】第一轮循环对数组中的最低位进行排序,最后将桶中的元素回填
【2】前提是数组中含有二位数,第二轮循环
【】最后一直比较到数组中最大值的位数轮
基数排序动画演示
基数排序代码示例
/**
* 获取数组中的最大值的位数
*/
public int getMaxDigit(int[] arr) {
int count = 1;
int max = Arrays.stream(arr).max().getAsInt();
while (max/10 > 0) {
count++;
max = max/10;
}
return count;
}
public void sort(int[] arr) {
if (arr==null||arr.length==0) {
return;
}
// 通过该变量控制每次取余的位数
int dev = 1;
// 获取数组中最大值的位数
int maxDigit = getMaxDigit(arr);
// 确定桶的数量
int bucketCount = 10;
for (int k = 0; k < maxDigit; k++) {
List<Integer>[] buckets = new List[bucketCount];
// 对桶进行初始化
for (int i = 0; i < bucketCount; i++) {
buckets[i] = new ArrayList<>();
}
// 将数据放入桶中
for (int i = 0; i < arr.length; i++) {
buckets[arr[i] / dev % 10].add(arr[i]);
}
// 对每个桶进行排序
for (int i = 0; i < bucketCount; i++) {
buckets[i].sort((o1, o2) -> o1 - o2);
}
int index = 0;
// 将桶中某个位数已经排好序的元素回填
for (int i = 0; i < bucketCount; i++) {
for (int j = 0; j < buckets[i].size(); j++) {
arr[index++] = buckets[i].get(j);
}
}
dev *= 10;
}
}