排序算法大总结、链表及其相关题目
写在前面
- 关于文章
本系列文章主要根据算法大神左神(左程云)的算法与数据结构基础教程进行整理的,这里文章主要学习了有排序算法大总结、链表结构在Java集合中的底层实现以及链表结构的相关算法题目等。这里只是基于Java的相关代码实现。
- 我的站点
- 联系我
🐧:
2410685763
WeChat:
Super_Baby_00
公众号:
编程那点事儿
常用算法总结
提醒:以下排序算法的算法步骤相关内容是在我个人理解下整理的,如果您在阅读过程中,感觉难以理解,可以直接阅读源码可能更容易理解,毕竟每个人的逻辑思路可能不同哦😋算法分析以及总结是在前人的总结下进行整理的,应该不会有错,都是固定的。
排序算法分类
十种常见排序算法可以分为两大类:
比较类排序
:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn)
,因此也称为非线性时间
比较类排序。非比较类排序
:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间
运行,因此也称为线性时间非比较类排序。
在这里主要总结记录相关排序算法的过程以及逻辑,简单核心代码以及算法分析。
冒泡排序
冒泡排序是一种简单的交换类排序
算法。对于给定的数组含n个元素,则需要进行(N-1)轮比较。每一轮比较可以确定一个最值的正确位置。每一轮从第一个开始对相邻元素进行比较,当前面的记录大于后面的记录,交换其位置,进行一轮比较和换位之后,最大记录在第n个位置;然后对前(n-1)个记录进行第二轮比较;重复该过程直到进行比较的记录只剩下一个时为止。
算法步骤
- 准备第一个for循环,用于控制比较轮数。
对于给定的数组含n个元素,则需要进行(N-1)轮比较
; - 准备第二个循环,进行每轮的比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一轮每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;直到排序完成。
算法实现
private static void bubbleSort(int[] arr){
boolean flag;
for (int i = 0; i < arr.length; i++){
flag = true;
for (int j = i + 1; j < arr.length; j++){
if (arr[j] < arr[i]){
swap(arr,i,j);
flag = false;
}
}
if (flag){
return;
}
}
}
算法分析
- 时间复杂度:最好时间复杂度:
O(n)
最坏时间复杂度:
O(n^2)
平均时间复杂度:
O(n^2)
- 空间复杂度:
O(1)
- 稳定性:可以实现
稳定
的排序
选择排序
选择排序是一种简单的选择类排序
算法。对于给定的一个数组,第一轮比较选择最小(或最大)的值,然后将该值与索引第一个进行交换;接着对不包括第一个确定的值进行第二轮比较,选择第二个最值记录与索引第二个位置进行交换,重复到只剩最后一个记录位置。
算法步骤
- 准备第一个for循环,用于控制比较轮数。
对于给定的数组含n个元素,则需要进行(N-1)轮比较
; - 每一轮找出一个最值将其与最左侧元素交换,使得整体分为有序和无序两部分;
- 每轮比较之前,将第一个元素标记为最值元素并记录其下标值;
- 准备第二个循环,从无序区域开始往后比较,找出无序区域的最值与第一个元素交换位置,有序区域不断扩大;
- n-1趟结束,数组有序化。
简单说就是依次让数组元素在0-1、0-2、0-3、0-4、......、0-N-1位置上依次都有序。所谓选择就是每轮在无序区域选择一个最值放到有序区域,使得整体逐渐有序。
算法实现
private static void OptionSort(int[] arr){
if (arr == null || arr.length < 2){
return;
}
for (int i = 0; i < arr.length - 1; i++) { // 确定几轮比较
int minIndex = i; //每次将第一个数记为最小
for (int j = i + 1; j < arr.length; j++) { //依次往后比较
minIndex = arr[j] < arr[minIndex] ? j : minIndex; //比第一个数小交换坐标
}
swap(arr, i, minIndex); //最后将第i个坐标的数字换成最小的 保证了前i个坐标的数字是有序的
}
}
算法分析
- 时间复杂度:
最好、最坏、平均的时间复杂度都为O(n^2)
- 空间复杂度:
O(1)
- 稳定性:
不稳定
插入排序
插入排序是一种简单的插入类排序
算法。对于给定的一个数组,初始时假设第一个记录自成一个有序序列,其余的记录为无序序列;接着从第二个记录开始,按照记录的大小依次将当前处理的记录插入到其之前的有序序列中,直至最后一个记录插入到有序序列中为止。
算法步骤
- 准备第一个for循环保证 0 ~ i 有序,0-0必然有序,所以i从1开始;
- 准备第二个for循环使得 j 为当前i的前一个元素 ,即j = i - 1
- 当j>=0 保证访问数组不越界 且第j个数大于其后边的数 交换位置
简单说就是准备一个无序数组,第一个已经有序,然后从第二个元素开始依次与前边有序的进行比较,当大于或等于某一元素时,说明已经找到正确位置停止与前边元素的比较,进行插入即可。依次将后边无序的元素与前边有序的数组进行比较插入到合适的位置,最终全部有序。
算法实现
private static void insertionSort(int[] arr){
if (arr == null || arr.length < 2){
return;
}
for (int i = 1; i < arr.length; i++){ // 保证 0 ~ i 有序
// j 为当前i的前一个元素 当j>=0 保证访问数组不越界 且第j个数大于其后边的数 交换位置
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--){
swap(arr,j,j + 1);
}
}
}
算法分析
- 时间复杂度最好时间复杂度:
O(n)
最坏时间复杂度:
O(n^2)
平均时间复杂度:
O(n^2)
- 空间复杂度:
O(1)
- 稳定性:
可以实现稳定的排序
快速排序
快速排序是一种复杂的交换类排序
算法。高效的排序算法,它采用“分而治之”的思想,把大的拆分为小的,小的再拆分为更小的。其原理是:对于一组给定的记录,通过一趟排序后,将原序列分为两部分,其中前部分的所有记录均比后部分的所有记录小,然后再依次对前后两部分的记录进行快速排序,递归该过程,直到序列中的所有记录均有序为止。
算法步骤
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
算法实现
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 方法重载 对于整个数组区间进行sort
quickSort(arr, 0, arr.length - 1);
}
// arr[l...r]进行排序
public static void quickSort(int[] arr, int l, int r) {
if (l < r) {
// 等概率随机选取一个元素与最右侧元素交换 实现随机快排
swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
// partition操作是按照最右侧元素为基准,将大于基准的元素放在右侧,小于基准的元素放左侧
// 最终返回等于区域的临界坐标
int[] p = partition(arr, l, r);
// p[0] 是等于区域的左边界 p[0] - 1则是小于区域的右边界
quickSort(arr, l, p[0] - 1); // 小于区继续进行partition
// p[1] 是等于区域的右边界 p[0] + 1则是大于区域的左边界
quickSort(arr, p[1] + 1, r); // 大于区继续进行partition
}
}
public static int[] partition(int[] arr, int l, int r) {
int less = l - 1; // 小于区域右边界
int more = r; // 大于区域左边界
// l当前元素的位置
while (l < more) {
// 当前数 < 划分值
if (arr[l] < arr[r]) {
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]) { // 当前数 > 划分值
swap(arr, --more, l);
} else {
l++;
}
}
swap(arr, more, r);
return new int[] { less + 1, more };
}
算法分析
- 时间复杂度最好时间复杂度:
O(nlogn)
最坏时间复杂度:
O(n^2)
平均时间复杂度:
O(nlogn)
- 空间复杂度:
O(nlogn)
- 稳定性:
不稳定
归并排序
利用递归与分治
技术将数据序列划分成为越来越小的半子表,再对半子表排序,最后再用递归步骤将排好序的半子表合并成为越来越大的有序序列。其中“归”代表的是递归的意思,即递归地将数组折半地分离为单个数组。
给定一个数组含n个元素,首先将每两个相邻的长度为1的子序列进行归并,得到n/2(向上取整)个长度为2或1的有序子序列,再将其两两归并,反复执行此过程,直到得到一个有序序列为止。
算法步骤
- 申请辅助空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
算法实现
private static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 方法重载 对于整个数组区间进行merge
mergeSort(arr, 0, arr.length - 1);
}
private static void mergeSort(int[] arr, int L, int R) {
// 如果获取区间为一个数 无需排序默认直接返回
if (L == R){
return;
}
// 获取指定区间中间位置
int mid = L + ((R - L) >> 1);
// 递归将指定大范围 分为每个小区间进行归并排序
mergeSort(arr, L, mid);
mergeSort(arr, mid + 1, R);
merge(arr, L, mid, R);
}
private static void merge(int[] arr, int L, int mid, int R) {
// 准备辅助数组空间 且大小和指定范围相同 用于保存排好序的数据
int[] help = new int[(R - L) + 1];
// 准备指针记录下标
int helpIndex = 0; //记录辅助数组空间下标
int p1 = L; //记录左部分起始下标
int p2 = mid + 1; //记录右部分起始下标
// 当左右两部分下标均不越界,比较所指两数大小,并将小数存入help 同时将help 和 小数部分下标++
while (p1 <= mid && p2 <= R) {
help[helpIndex++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
// 当有一边下标越界,跳出第一个循环
// 将不越界的一边剩余数据全部拷贝到help中
// 以下两个循环必定只执行一个
while (p1 <= mid) {
help[helpIndex++] = arr[p1++];
}
while (p2 <= R) {
help[helpIndex++] = arr[p2++];
}
// 拷贝help数组到原有数组即可!
for (int i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
算法分析
- 时间复杂度:
最好、最坏和平均情况O(nlogn)
- 空间复杂度:
O(n)
- 稳定性:
稳定
推排序
堆是一种特殊的树形数据结构,其每个结点都有一个值,通常提到的堆都是指一棵完全二叉树,根结点的值小于(或大于)两个子结点的值,同时根结点的两个子树也分别是一个堆。
堆排序是一种复杂的的选择类排序
算法。对于给定的一个数组中,初始把这些记录看成一颗顺序存储的二叉树,然后将其调整为一个大顶堆,然后将堆的最后一个元素与堆顶元素进行交换后,堆的最后一个元素即为最大记录;接着将前(n-1)个元素重新调整为一个大顶堆,在将堆顶元素与当前堆的最后一个元素进行交换后得到次大的记录,重复该过程直到调整的堆中只剩一个元素为止,该记录即为最小记录,此时可得到一个有序序列。
算法步骤
整个堆排序过程可以分为:1. 构建堆;2. 交换堆顶元素与最后一个元素的位置
- 首先需要将数组全部元素构成一个堆结构(大顶/小顶)首先准备for循环依次遍历每个元素,然后进入while循环当该元素值大于其父节点元素值时交换二者,直到将该元素调整到合适位置。最终形成堆结构。
- 将堆结构中最后一个元素与堆顶置换循环将数组中各个元素都与堆顶置换进行
堆化操作
重新构建堆结构,以便找到最值。堆化操作详细步骤看heapify代码注释!
算法实现
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 将数组全部元素构成堆结构
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
//
int size = arr.length;
// 将堆结构中最后一个元素与堆顶置换
swap(arr, 0, --size);
// 循环将数组中各个元素都与堆顶置换进行重新构建堆结构,以便找到最值
while (size > 0) {
heapify(arr, 0, size); // 堆化操作,将堆顶元素找到正确位置
// --size因为每次可以确定一个元素的最终位置
swap(arr, 0, --size);
}
}
public static void heapInsert(int[] arr, int index) {
// 当元素值大于其父节点元素值时
while (arr[index] > arr[(index - 1) / 2]) {
// 交换二者
swap(arr, index, (index - 1) /2);
// 更新元素当前的下标
index = (index - 1)/2 ;
}
}
public static void heapify(int[] arr, int index, int size) {
// 获取左孩子下标
int left = index * 2 + 1;
// 当有左孩子时
while (left < size) {
// 比较左右孩子的值,获取二者的较大值的元素下标
// left + 1 < size 确定是否有右孩子
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
// 比较左右孩子较大者是否比父节点大
largest = arr[largest] > arr[index] ? largest : index;
// 如果相等 则跳出无需继续向下比较
if (largest == index) {
break;
}
// 如果不相等 则交换两者的值
swap(arr, largest, index);
// 继续向下比较
// 更新根节点的下标值
index = largest;
// 更新左孩子的下标值
left = index * 2 + 1;
}
}
算法分析
时间复杂度: 主要耗费在创建堆和反复调整堆上,最坏情况下,时间复杂度也为O(nlogn)
空间复杂度:O(1)
稳定性: 不稳定
以下几种排序算法,目前没有重点掌握,后期会把以下几种算法进行详细描述。
希尔排序
1959年Shell发明,第一个突破O(n^2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版》的合著者Robert Sedgewick提出的。
算法步骤
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
算法实现
function shellSort(arr) {
var len = arr.length,
temp,
gap = 1;
while (gap < len/3) { //动态定义间隔序列
gap =gap*3+1;
}
for (gap; gap> 0; gap = Math.floor(gap/3)) {
for ( var i = gap; i < len; i++) {
temp = arr[i];
for ( var j = i-gap; j > 0 && arr[j]> temp; j-=gap) {
arr[j+gap] = arr[j];
}
arr[j+gap] = temp;
}
}
return arr;
}
算法分析
最佳情况:T(n) = O(nlog2 n) 最坏情况:T(n) = O(nlog2 n) 平均情况:T(n) =O(nlog n)
计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。
算法步骤
找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
动图演示
算法实现
function countingSort(arr, maxValue) {
var bucket = new Array(maxValue+1),
sortedIndex = 0;
arrLen = arr.length,
bucketLen = maxValue + 1;
for ( var i = 0; i < arrLen; i++) {
if (!bucket[arr[i]]) {
bucket[arr[i]] = 0;
}
bucket[arr[i]]++;
}
for ( var j = 0; j < bucketLen; j++) {
while (bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
return arr;
}
算法分析
当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n+k)
桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排
算法步骤
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
算法实现
function bucketSort(arr, bucketSize) {
if (arr.length === 0) {
return arr;
}
var i;
var minValue = arr[0];
var maxValue = arr[0];
for (i = 1; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i]; //输入数据的最小值
} else if (arr[i] > maxValue) {
maxValue = arr[i]; //输入数据的最大值
}
}
//桶的初始化
var DEFAULT_BUCKET_SIZE = 5; //设置桶的默认数量为5
bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
var buckets = new Array(bucketCount);
for (i = 0; i < buckets.length; i++) {
buckets[i] = [];
}
//利用映射函数将数据分配到各个桶中
for (i = 0; i < arr.length; i++) {
buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
}
arr.length = 0;
for (i = 0; i < buckets.length; i++) {
insertionSort(buckets[i]); //对每个桶进行排序,这里使用了插入排序
for ( var j = 0; j < buckets[i].length; j++) {
arr.push(buckets[i][j]);
}
}
return arr;
}
算法分析
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n2)
基数排序
基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数;
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
算法步骤
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
算法实现
var counter = [];
function radixSort(arr, maxDigit) {
var mod = 10;
var dev = 1;
for ( var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
for ( var j = 0; j < arr.length; j++) {
var bucket = parseInt((arr[j] % mod) / dev);
if (counter[bucket]== null ) {
counter[bucket] = [];
}
counter[bucket].push(arr[j]);
}
var pos = 0;
for ( var j = 0; j < counter.length; j++) {
var value = null ;
if (counter[j]!= null ) {
while ((value = counter[j].shift()) != null ) {
arr[pos++] = value;
}
}
}
}
return arr;
}
算法分析
最佳情况:T(n) = O(n * k) 最差情况:T(n) = O(n * k) 平均情况:T(n) = O(n * k)
基数排序有两种方法:
MSD 从高位开始进行排序 LSD 从低位开始进行排序
分析小结
桶排序思想下的排序
1)桶排序思想下的排序都是不基于比较的排序
2)时间复杂度为O(N),额外空间负载度O(M)
3)应用范围有限,需要样本的数据状况满足桶的划分
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶
- 计数排序:每个桶只存储单一键值
- 桶排序:每个桶存储一定范围的数值
相关概念
稳定
:在原数组中,如果a原本在b前面,而a=b,排序之后a仍然在b的前面。不稳定
:在原数组中,如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。时间复杂度
:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。空间复杂度
:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。- 内排序:所有排序操作都在内存中完成。
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行。
关于时间复杂度
平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序
。
线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序
;
O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
关于稳定性
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序
。
名词解释:
- n:数据规模
- k:"桶"的个数
- In-place:占用常数内存,不占用额外内存
- Out-place:占用额外内存
- 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
一定要根据数据的规模、规律来给出合适的算法,不能觉得快速排序名字就以为是快速的,切记不能什么排序问题都回答快排。
- 虽然插入排序和冒泡排序平均速度较慢,但当初始序列整体或局部有序时,这两者效率较高
- 排序数据较小,且不要求稳定的情况下,选择排序效率较高;要求稳定,选择冒泡排序。
- 堆排序在更大的序列上往往优于快速排序和归并排序。
- 针对小数据追求线性时间复杂度,考虑计数排序和桶排序
- 除了上面几种常见的排序算法,还有众多其他排序算法,每种排序算法都有其最佳适用场合。具体情况具体分析。
工程上对排序的改进:就是充分利用O(N*logN)和O(N^2)排序各自的优势 和稳定性的考虑,进行多种排序算法的结合
避坑
目前没有找到时间复杂度O(N*logN),额外空间复杂度O(1),又稳定的排序。
// 左神总结常见的坑
1,归并排序的额外空间复杂度可以变成O(1),但是非常难,不需要掌握,有兴趣可以搜“归并排序 内部缓存法”
2,“原地归并排序”的帖子都是垃圾,会让归并排序的时间复杂度变成O(N^2)
3,快速排序可以做到稳定性问题,但是非常难,不需要掌握, 可以搜“01stable sort”
4,所有的改进都不重要,因为目前没有找到时间复杂度O(N*logN),额外空间复杂度O(1),又稳定的排序。
5,有一道题目,是奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变,碰到这个问题,可以怼面试官。
链表在Java集合底层使用
在这里只是简单学习下链表结构在Java 相关集合结构的底层实现的应用,并不涉及相关数据结构的原理。
哈希表的简单介绍
1)哈希表在使用层面上可以理解为一种集合结构
2)如果只有key,没有伴随数据value,可以使用HashSet结构
(C++中叫UnOrderedSet)
3)如果既有key,又有伴随数据value,可以使用HashMap结构
(C++中叫UnOrderedMap)
4)有无伴随数据,是HashMap和HashSet唯一的区别,底层的实际结构是一回事
5)使用哈希表增(put)、删(remove)、改(put)和查(get)的操作,可以认为时间复杂度为 O(1),但是常数时间比较大
6)放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小
7)放入哈希表的东西,如果不是基础类型,内部按引用传递,内存占用是这个东西内存地址的大小 ,统一为8bit
有序表的简单介绍
1)有序表在使用层面上可以理解为一种集合结构
2)如果只有key,没有伴随数据value,可以使用TreeSet结构
(C++中叫OrderedSet)
3)如果既有key,又有伴随数据value,可以使用TreeMap结构
(C++中叫OrderedMap)
4)有无伴随数据,是TreeSet和TreeMap唯一的区别,底层的实际结构是一回事
5)有序表和哈希表的区别是,有序表把key按照顺序组织起来,而哈希表完全不组织
红黑树、AVL树、size-balance-tree和跳表等都属于有序表结构,只是底层具体实现 不同
6)放入有序表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小
7)放入有序表的东西,如果不是基础类型,必须提供比较器,内部按引用传递,内存占用是这个东西内存地址的大小
8)不管是什么底层具体实现,只要是有序表,都有以下固定的基本功能和固定的时间复杂度O(logN)
有序表的固定操作
1)void put(K key, V value):将一个(key,value)记录加入到表中,或者将key的记录更新成value。
2)V get(K key):根据给定的key,查询value并返回。
3)void remove(K key):移除key的记录。
4)boolean containsKey(K key):询问是否有关于key的记录。
5)K firstKey():返回所有键值的排序结果中,最左(最小)的那个。
6)K lastKey():返回所有键值的排序结果中,最右(最大)的那个。
7)K floorKey(K key):如果表中存入过key,返回key;否则返回所有键值的排序结果中,key的前一个。
8)K ceilingKey(K key):如果表中存入过key,返回key;否则返回所有键值的排序结果中,
key的后一个。
以上所有操作时间复杂度都是O(logN)
,N为有序表含有的记录数
代码演示
public class HashAndTree {
// 内部类 定义节点类型
public static class Node {
public int value;
public Node next;
public Node(int val) {
value = val;
}
}
// 定义节点类型数据比较器
public static class NodeComparator implements Comparator<Node> {
@Override
public int compare(Node o1, Node o2) {
return o1.value - o2.value;
}
}
public static void main(String[] args) {
Node nodeA = null;
Node nodeB = null;
Node nodeC = null;
// hashSet1的key是基础类型->int类型
HashSet<Integer> hashSet1 = new HashSet<>();
hashSet1.add(3);
System.out.println(hashSet1.contains(3));
hashSet1.remove(3);
System.out.println(hashSet1.contains(3));
System.out.println("========1=========");
// hashSet2的key是非基础类型->Node类型
nodeA = new Node(1);
nodeB = new Node(1);
HashSet<Node> hashSet2 = new HashSet<>();
hashSet2.add(nodeA);
System.out.println(hashSet2.contains(nodeA));
System.out.println(hashSet2.contains(nodeB));
hashSet2.remove(nodeA);
System.out.println(hashSet2.contains(nodeA));
System.out.println("========2=========");
// hashMap1的key是基础类型->String类型
HashMap<String, Integer> hashMap1 = new HashMap<>();
String str1 = "key";
String str2 = "key";
hashMap1.put(str1, 1);
System.out.println(hashMap1.containsKey(str1));
System.out.println(hashMap1.containsKey(str2));
System.out.println(hashMap1.get(str1));
System.out.println(hashMap1.get(str2));
hashMap1.put(str2, 2);
System.out.println(hashMap1.containsKey(str1));
System.out.println(hashMap1.containsKey(str2));
System.out.println(hashMap1.get(str1));
System.out.println(hashMap1.get(str2));
hashMap1.remove(str1);
System.out.println(hashMap1.containsKey(str1));
System.out.println(hashMap1.containsKey(str2));
System.out.println("========3=========");
// hashMap2的key是非基础类型->Node类型
nodeA = new Node(1);
nodeB = new Node(1);
HashMap<Node, String> hashMap2 = new HashMap<>();
hashMap2.put(nodeA, "A节点");
System.out.println(hashMap2.containsKey(nodeA));
System.out.println(hashMap2.containsKey(nodeB));
System.out.println(hashMap2.get(nodeA));
System.out.println(hashMap2.get(nodeB));
hashMap2.put(nodeB, "B节点");
System.out.println(hashMap2.containsKey(nodeA));
System.out.println(hashMap2.containsKey(nodeB));
System.out.println(hashMap2.get(nodeA));
System.out.println(hashMap2.get(nodeB));
System.out.println("========4=========");
// treeSet的key是非基础类型->Node类型
nodeA = new Node(5);
nodeB = new Node(3);
nodeC = new Node(7);
TreeSet<Node> treeSet = new TreeSet<>();
// 以下的代码会报错,因为没有提供Node类型的比较器
try {
treeSet.add(nodeA);
treeSet.add(nodeB);
treeSet.add(nodeC);
} catch (Exception e) {
System.out.println("错误信息:" + e.getMessage());
}
treeSet = new TreeSet<>(new NodeComparator());
// 以下的代码没问题,因为提供了Node类型的比较器
try {
treeSet.add(nodeA);
treeSet.add(nodeB);
treeSet.add(nodeC);
System.out.println("这次节点都加入了");
} catch (Exception e) {
System.out.println(e.getMessage());
}
System.out.println("========5=========");
// 展示有序表常用操作
TreeMap<Integer, String> treeMap1 = new TreeMap<>();
treeMap1.put(7, "我是7");
treeMap1.put(5, "我是5");
treeMap1.put(4, "我是4");
treeMap1.put(3, "我是3");
treeMap1.put(9, "我是9");
treeMap1.put(2, "我是2");
System.out.println(treeMap1.containsKey(5));
System.out.println(treeMap1.get(5));
System.out.println(treeMap1.firstKey() + ", 我最小");
System.out.println(treeMap1.lastKey() + ", 我最大");
System.out.println(treeMap1.floorKey(8) + ", 在表中所有<=8的数中,我离8最近");
System.out.println(treeMap1.ceilingKey(8) + ", 在表中所有>=8的数中,我离8最近");
System.out.println(treeMap1.floorKey(7) + ", 在表中所有<=7的数中,我离7最近");
System.out.println(treeMap1.ceilingKey(7) + ", 在表中所有>=7的数中,我离7最近");
treeMap1.remove(5);
System.out.println(treeMap1.get(5) + ", 删了就没有了哦");
System.out.println("========6=========");
}
}
链表相关算法题目
算法题目后期会陆续填补实现代码,目前先把相关题目扔到这里!
反转单向和双向链表
【题目】 分别实现反转单向链表和反转双向链表的函数
【要求】 如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为 O(1)
打印两个有序链表公共部分
【题目】 给定两个有序链表的头指针head1和head2,打印两个链表的公共部分。
【要求】 如果两个链表的长度之和为N,时间复杂度要求为O(N),额外空间复 杂度要求为O(1)
判断一个链表是否为回文结构
【题目】给定一个单链表的头节点head,请判断该链表是否为回文结构。
【例子】1->2->1,返回true; 1->2->2->1,返回true;15->6->15,返回true; 1->2->3,返回false。
【例子】如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1)。
题解:
剑指 Offer II 027. 回文链表 - 编程那点事儿 (imyjs.cn)
将单向链表按某值划分成左边小、中间相等、右边大的形式
【题目】给定一个单链表的头节点head,节点的值类型是整型,再给定一个整 数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于pivot的 节点,中间部分都是值等于pivot的节点,右部分都是值大于pivot的节点。
【进阶】在实现原问题功能的基础上增加如下的要求
【要求】调整后所有小于pivot的节点之间的相对顺序和调整前一样
【要求】调整后所有等于pivot的节点之间的相对顺序和调整前一样
【要求】调整后所有大于pivot的节点之间的相对顺序和调整前一样
【要求】时间复杂度请达到O(N),额外空间复杂度请达到O(1)
复制含有随机指针节点的链表
【题目】一种特殊的单链表节点类描述如下
class Node {
int value;
Node next;
Node rand;
Node(int val) {
value = val;
}
}
rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节 点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点 head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。
【要求】时间复杂度O(N),额外空间复杂度O(1)
两个单链表相交的一系列问题
【题目】给定两个可能有环也可能无环的单链表,头节点head1和head2。请实 现一个函数,如果两个链表相交,请返回相交的 第一个节点。如果不相交,返 回null
【要求】如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度 请达到O(1)。