leetcode-master/problems/0406.根据身高重建队列.md

406 lines
15 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center">
<a href="https://programmercarl.com/other/xunlianying.html" target="_blank">
<img src="../pics/训练营.png" width="1000"/>
</a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
# 406.根据身高重建队列
[力扣题目链接](https://leetcode.cn/problems/queue-reconstruction-by-height/)
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性queue[0] 是排在队列前面的人)。
示例 1
* 输入people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
* 输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
* 解释:
* 编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
* 编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
* 编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
* 编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
* 编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
* 编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
* 因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。
示例 2
* 输入people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
* 输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
提示:
* 1 <= people.length <= 2000
* 0 <= hi <= 10^6
* 0 <= ki < people.length
题目数据确保队列可以被重建
## 算法公开课
**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)[贪心算法不要两边一起贪会顾此失彼 | LeetCode406.根据身高重建队列](https://www.bilibili.com/video/BV1EA411675Y)相信结合视频在看本篇题解更有助于大家对本题的理解**。
## 思路
本题有两个维度h和k看到这种题目一定要想如何确定一个维度然后再按照另一个维度重新排列
其实如果大家认真做了[135. 分发糖果](https://programmercarl.com/0135.分发糖果.html)就会发现和此题有点点的像
[135. 分发糖果](https://programmercarl.com/0135.分发糖果.html)我就强调过一次遇到两个维度权衡的时候一定要先确定一个维度再确定另一个维度
**如果两个维度一起考虑一定会顾此失彼**
对于本题相信大家困惑的点是先确定k还是先确定h呢也就是究竟先按h排序呢还是先按照k排序呢
如果按照k来从小到大排序排完之后会发现k的排列并不符合条件身高也不符合条件两个维度哪一个都没确定下来
那么按照身高h来排序呢身高一定是从大到小排身高相同的话则k小的站前面让高个子在前面
**此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高!**
那么只需要按照k为下标重新插入队列就可以了为什么呢
以图中{5,2} 为例
![406.根据身高重建队列](https://code-thinking-1253855093.file.myqcloud.com/pics/20201216201851982.png)
按照身高排序之后优先按身高高的people的k来插入后序插入节点也不会影响前面已经插入的节点最终按照k的规则完成了队列
所以在按照身高从大到小排序后
**局部最优优先按身高高的people的k来插入。插入操作过后的people满足队列属性**
**全局最优:最后都做完插入操作,整个队列满足题目队列属性**
局部最优可推出全局最优找不出反例那就试试贪心
一些同学可能也会疑惑你怎么知道局部最优就可以推出全局最优呢 有数学证明么
在贪心系列开篇词[关于贪心算法你该了解这些](https://programmercarl.com/贪心算法理论基础.html)我已经讲过了这个问题了
刷题或者面试的时候手动模拟一下感觉可以局部最优推出整体最优而且想不到反例那么就试一试贪心至于严格的数学证明就不在讨论范围内了
如果没有读过[关于贪心算法你该了解这些](https://programmercarl.com/贪心算法理论基础.html)的同学建议读一下相信对贪心就有初步的了解了
回归本题整个插入过程如下
排序完的people
[[7,0], [7,1], [6,1], [5,0], [5,2][4,4]]
插入的过程
* 插入[7,0][[7,0]]
* 插入[7,1][[7,0],[7,1]]
* 插入[6,1][[7,0],[6,1],[7,1]]
* 插入[5,0][[5,0],[7,0],[6,1],[7,1]]
* 插入[5,2][[5,0],[7,0],[5,2],[6,1],[7,1]]
* 插入[4,4][[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
此时就按照题目的要求完成了重新排列
C++代码如下
```CPP
// 版本一
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b) {
if (a[0] == b[0]) return a[1] < b[1];
return a[0] > b[0];
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort (people.begin(), people.end(), cmp);
vector<vector<int>> que;
for (int i = 0; i < people.size(); i++) {
int position = people[i][1];
que.insert(que.begin() + position, people[i]);
}
return que;
}
};
```
* 时间复杂度O(nlog n + n^2)
* 空间复杂度O(n)
但使用vector是非常费时的C++中vector可以理解是一个动态数组底层是普通数组实现的如果插入元素大于预先普通数组大小vector底部会有一个扩容的操作即申请两倍于原先普通数组的大小然后把数据拷贝到另一个更大的数组上
所以使用vector动态数组来insert是费时的插入再拷贝的话单纯一个插入的操作就是O(n^2)甚至可能拷贝好几次就不止O(n^2)
改成链表之后C++代码如下
```CPP
// 版本二
class Solution {
public:
// 身高从大到小排身高相同k小的站前面
static bool cmp(const vector<int>& a, const vector<int>& b) {
if (a[0] == b[0]) return a[1] < b[1];
return a[0] > b[0];
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort (people.begin(), people.end(), cmp);
list<vector<int>> que; // list底层是链表实现插入效率比vector高的多
for (int i = 0; i < people.size(); i++) {
int position = people[i][1]; // 插入到下标为position的位置
std::list<vector<int>>::iterator it = que.begin();
while (position--) { // 寻找在插入位置
it++;
}
que.insert(it, people[i]);
}
return vector<vector<int>>(que.begin(), que.end());
}
};
```
* 时间复杂度O(nlog n + n^2)
* 空间复杂度O(n)
大家可以把两个版本的代码提交一下试试就可以发现其差别了
关于本题使用数组还是使用链表的性能差异我在[贪心算法根据身高重建队列续集](https://programmercarl.com/根据身高重建队列vector原理讲解.html)中详细讲解了一波
## 总结
关于出现两个维度一起考虑的情况我们已经做过两道题目了另一道就是[135. 分发糖果](https://programmercarl.com/0135.分发糖果.html)。
**其技巧都是确定一边然后贪心另一边,两边一起考虑,就会顾此失彼**
这道题目可以说比[135. 分发糖果](https://programmercarl.com/0135.分发糖果.html)难不少其贪心的策略也是比较巧妙
最后我给出了两个版本的代码可以明显看是使用C++中的list底层链表实现比vector数组效率高得多
**对使用某一种语言容器的使用,特性的选择都会不同程度上影响效率**
所以很多人都说写算法题用什么语言都可以主要体现在算法思维上其实我是同意的但也不同意
对于看别人题解的同学题解用什么语言其实影响不大只要题解把所使用语言特性优化的点讲出来大家都可以看懂并使用自己语言的时候注意一下
对于写题解的同学刷题用什么语言影响就非常大如果自己语言没有学好而强调算法和编程语言没关系其实是会误伤别人的
**这也是我为什么统一使用C++写题解的原因**
## 其他语言版本
### Java
```java
class Solution {
public int[][] reconstructQueue(int[][] people) {
// 身高从大到小排身高相同k小的站前面
Arrays.sort(people, (a, b) -> {
if (a[0] == b[0]) return a[1] - b[1]; // a - b 是升序排列故在a[0] == b[0]的狀況下會根據k值升序排列
return b[0] - a[0]; //b - a 是降序排列在a[0] != b[0]的狀況會根據h值降序排列
});
LinkedList<int[]> que = new LinkedList<>();
for (int[] p : people) {
que.add(p[1],p); //Linkedlist.add(index, value)會將value插入到指定index裡。
}
return que.toArray(new int[people.length][]);
}
}
```
### Python
```python
class Solution:
def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
# 先按照h维度的身高顺序从高到低排序。确定第一个维度
# lambda返回的是一个元组当-x[0](维度h相同时再根据x[1]维度k从小到大排序
people.sort(key=lambda x: (-x[0], x[1]))
que = []
# 根据每个元素的第二个维度k贪心算法进行插入
# people已经排序过了同一高度时k值小的排前面。
for p in people:
que.insert(p[1], p)
return que
```
### Go
```go
func reconstructQueue(people [][]int) [][]int {
// 先将身高从大到小排序,确定最大个子的相对位置
sort.Slice(people, func(i, j int) bool {
if people[i][0] == people[j][0] {
return people[i][1] < people[j][1] // 当身高相同时将K按照从小到大排序
}
return people[i][0] > people[j][0] // 身高按照由大到小的顺序来排
})
// 再按照K进行插入排序优先插入K小的
for i, p := range people {
copy(people[p[1]+1 : i+1], people[p[1] : i+1]) // 空出一个位置
people[p[1]] = p
}
return people
}
```
```go
// 链表实现
func reconstructQueue(people [][]int) [][]int {
sort.Slice(people,func (i,j int) bool {
if people[i][0] == people[j][0] {
return people[i][1] < people[j][1] //当身高相同时将K按照从小到大排序
}
return people[i][0] > people[j][0]
})
l := list.New() //创建链表
for i:=0; i < len(people); i++ {
position := people[i][1]
mark := l.PushBack(people[i]) //插入元素
e := l.Front()
for position != 0 { //获取相对位置
position--
e = e.Next()
}
l.MoveBefore(mark, e) //移动位置
}
res := [][]int{}
for e := l.Front(); e != nil; e = e.Next() {
res = append(res, e.Value.([]int))
}
return res
}
```
### Javascript
```Javascript
var reconstructQueue = function(people) {
let queue = []
people.sort((a, b ) => {
if(b[0] !== a[0]) {
return b[0] - a[0]
} else {
return a[1] - b[1]
}
})
for(let i = 0; i < people.length; i++) {
queue.splice(people[i][1], 0, people[i])
}
return queue
};
```
### Rust
```Rust
impl Solution {
pub fn reconstruct_queue(mut people: Vec<Vec<i32>>) -> Vec<Vec<i32>> {
let mut queue = vec![];
people.sort_by(|a, b| {
if a[0] == b[0] {
return a[1].cmp(&b[1]);
}
b[0].cmp(&a[0])
});
queue.push(people[0].clone());
for v in people.iter().skip(1) {
queue.insert(v[1] as usize, v.clone());
}
queue
}
}
```
### C
```c
int cmp(const void *p1, const void *p2) {
int *pp1 = *(int**)p1;
int *pp2 = *(int**)p2;
// 若身高相同则按照k从小到大排列
// 若身高不同,按身高从大到小排列
return pp1[0] == pp2[0] ? pp1[1] - pp2[1] : pp2[0] - pp1[0];
}
// 将start与end中间的元素都后移一位
// start为将要新插入元素的位置
void moveBack(int **people, int peopleSize, int start, int end) {
int i;
for(i = end; i > start; i--) {
people[i] = people[i-1];
}
}
int** reconstructQueue(int** people, int peopleSize, int* peopleColSize, int* returnSize, int** returnColumnSizes){
int i;
// 将people按身高从大到小排列若身高相同按k从小到大排列
qsort(people, peopleSize, sizeof(int*), cmp);
for(i = 0; i < peopleSize; ++i) {
// people[i]要插入的位置
int position = people[i][1];
int *temp = people[i];
// 将position到i中间的元素后移一位
// 注因为已经排好序position不会比i大。(举例排序后people最后一位元素最小其可能的k最大值为peopleSize-2小于此时的i)
moveBack(people, peopleSize, position, i);
// 将temp放置到position处
people[position] = temp;
}
// 设置返回二维数组的大小以及里面每个一维数组的长度
*returnSize = peopleSize;
*returnColumnSizes = (int*)malloc(sizeof(int) * peopleSize);
for(i = 0; i < peopleSize; ++i) {
(*returnColumnSizes)[i] = 2;
}
return people;
}
```
### TypeScript
```typescript
function reconstructQueue(people: number[][]): number[][] {
people.sort((a, b) => {
if (a[0] === b[0]) return a[1] - b[1];
return b[0] - a[0];
});
const resArr: number[][] = [];
for (let i = 0, length = people.length; i < length; i++) {
resArr.splice(people[i][1], 0, people[i]);
}
return resArr;
};
```
### Scala
```scala
object Solution {
import scala.collection.mutable
def reconstructQueue(people: Array[Array[Int]]): Array[Array[Int]] = {
val person = people.sortWith((a, b) => {
if (a(0) == b(0)) a(1) < b(1)
else a(0) > b(0)
})
var que = mutable.ArrayBuffer[Array[Int]]()
for (per <- person) {
que.insert(per(1), per)
}
que.toArray
}
}
```
<p align="center">
<a href="https://programmercarl.com/other/kstar.html" target="_blank">
<img src="../pics/网站星球宣传海报.jpg" width="1000"/>
</a>