【数据结构】算法数据结构

7 堆和排序堆

(1)堆的底层实现(堆的两种操作)

  1. 加进来一个数,heapsize+1,并构造大根堆

新加进来的数,现在停在了index位置,请依次往上移动,移动到0位置,或者比自己父亲小,停止。时间复杂度O(logN)) 跟高度有关

private void heapInsert (int[] arr, int index) {
	while (arr[index] > arr[(index - 1) / 2]) {
		swap (arr, index, (index - 1) / 2);
		index = (index - 1) / 2;
	}
}
  1. 不再加数,返回一个最大值,并且把这个最大值从大根堆中删除,剩下的数再调整为大根堆

先根节点的值记录,后与最下面的子节点交换,再将heapSize - 1。
从index位置(index交换后应该是来到了根节点),往下看,不断地下沉,我较大的子节点都不再比我大或者没有子节点时候停止。时间复杂度O(logN))

private void heapify (int[] arr, int index, int heapSize) {
	left = index * 2 + 1;
	while (left > heapSize) { // 看是否越界
		int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
		largest = arr[index] > arr[largest] ? index : largest;
		if (largest == index) {
			break;
		}
		swap (arr, largest, index);
		index = largest;
		left = index * 2 + 1;
	}
}

(2)堆在Java中的应用

  • 堆在系统中默认为小根堆
  • heap:查看堆顶元素,不删除堆顶
  • poll:依次弹出最小(大)值,把这个堆弹空
public static void main (String[] args) {
	PriorityQueue<Integer> heap = new PriorityQueue<>();
	heap.add (5);
	heap.add (3);
	System.out.println (heap.peek ());
	while (! heap.isEmpty ()) {
		System.out.println (heap.poll ());
	}
}

(3)heapify和heapInsert的应用

  • 一个数值如果一个位置值发生了变化,已知该位置,在该位置顺序执行heapify和heapInsert即可调整回大根堆(小根堆),其中必有一个可以执行。

(4)默认小根堆如何变为大根堆

  • 设置一个整数的比较器,重新定义两个数怎么比大小
  • 堆可以加重复值的,有序表不可以
public static class MyComparator implements Comparator<Integer> {
	@Override
	public int compare (Integer o1, Interger o2) {
		return o2 - o1;
	}
}
public static void main (String[] args) {
	PriorityQueue<Integer> heap = new PriorityQueue<>(new MyComparator ());
	heap.add (5);
	heap.add (3);
	heap.add (7);
	heap.add (0);
	heap.add (5);
	heap.add (3);
	heap.add (7);
	heap.add (0);
	while (!heap.isEmpty ()) {
		System.out.println (heap.poll ());
	}
}

(5)堆排序

  1. 先让整个数组都变成大根堆结构,建立堆的过程:
  2. 把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆。一直周而复始,时间复杂度为O(N * logN)
  3. 堆的小大减小成0之后,排序完成
public static void heapSort (int[] arr) {
	if (arr == 0 || arr.length < 2) {
		return;
	}
	for (int i = 0; i < arr.length; i++) {
		heapInsert (arr, i);
	}
	int heapSize = arr.length;
	swap (arr, 0, --heapSize);
	while (headSize > 0) {
		heapify (arr, 0, heapSize);
		swap (arr, 0, --heapSize);
	}
}

堆排序的时间复杂度

太难了,改天再研究!

(6)与堆有关的题

  • 已知一个几乎有序的数组,几乎是指,如果给他排好序的话,每个元素移动的距离一定不超过k,并且k相对于数组长度来说是比较小的。
  • 请选择一个合适的排序策略,对这个数组进行排序
    思路:把0到k的数字放进小根堆(k+1个数),然后弹出最小值依次放到0,1,2…位置,在放进去k+1位置的数据,加一个弹一个,复杂度O(N*logk)
public static void sortedArrDistanceLessK (int[] arr, int k) {
	if (k == 0) {
		return;
	}
	PriorityQueue<Integer> heap = new PriorityQueue<>();
	int index = 0;
	for (; index <= Math.min (arr.length - 1, k - 1); index++) {
		heap.add (arr[index]);
	}
	int i = 0;
	for (; index < arr.length; i++, index++) {
		heap.add (arr[index]);
		arr[i] = heap.poll;
	}
	while (!heap.isEmpty ()) {
		arr[i++] = heap.poll ();
	}
}

8 加强堆

(1)最大线段重合问题(用堆的实现)

给定很多线段,每个线段都有两个数[start, end],表示线段开始位置和结束位置,左右都是闭区间。规定:
1)线段的开始和结束位置一定都是整数值
2)线段重合区域的长度必须>=1
返回线段最多重合区域中,包含了几条线段
随机生成线段

public static int[][] generateLines(int N, int L, int R) {
		int size = (int) (Math.random() * N) + 1;
		int[][] ans = new int[size][2];
		for (int i = 0; i < size; i++) {
			int a = L + (int) (Math.random() * (R - L + 1));
			int b = L + (int) (Math.random() * (R - L + 1));
			if (a == b) {
				b = a + 1;
			}
			ans[i][0] = Math.min(a, b);
			ans[i][1] = Math.max(a, b);
		}
		return ans;
	}

思路1
从最小长度的线段开始,计算在1.5-2.5中间有多少线段重合记录个数cnt,2.5-3.5之前有多少线段重合记录个数,然后3.5-4.5依次计算…直到最大值,最大的cnt为所求结果

public static int maxCover1 (int[][] lines) {
	int min = Integer.MAX_VALUE;
	int max = Integer.MIN_VALUE;
	for (int i = 0; i < lines.length; i++) {
		min = Math.min (min, lines[i][0]);
		max = Math.max (max, lines[i][1]);
	}
	int cover = 0;
	for (double p = min + 0.5; p < max; p += 1) {
		int cur = 0;
		for (int i = 0; i < lines.length; i++) {
			if (lines[i][0] < p && lines[i][1] > p) {
				cur++;
			}
		}
		cover = Math.max (cover, cur);
	}
	return cover;
}

思路2
1.先将所有线段开始位置从小到大进行排序,再准备一个小根堆P,存放线段的结束位置
2.将线段1的开始位置a与小根堆P里的数据进行比较,如果有小于线段1的开始位置a则将P中小于的数字全部弹出,再将线段1的结束位置b放入P中,如果都大于线段1的开始位置,则不用弹出直接放入结束位置数据,然后进行小根堆排序,并记录小根堆里的数据个数ans
3.按照线段大小依次进行2步骤,最后将最大ans返回即为所求

public static int maxCover2 (int[][] m) {
public static int maxCover2(int[][] m) {
		Line[] lines = new Line[m.length];
		for (int i = 0; i < m.length; i++) {
			lines[i] = new Line(m[i][0], m[i][1]);
		}
		Arrays.sort(lines, new StartComparator());
		// 小根堆,每一条线段的结尾数值,使用默认的
		PriorityQueue<Integer> heap = new PriorityQueue<>();
		int max = 0;
		for (int i = 0; i < lines.length; i++) {
			// lines[i] -> cur 在黑盒中,把<=cur.start 东西都弹出
			while (!heap.isEmpty () && heap.peek() <= lines[i].start) {
				heap.poll ();
			}
			heap.add(lines[i].end);
			max = Math.max(max, heap.size());
		}
		return max;
	}

	public static class Line {
		public int start;
		public int end;

		public Line(int s, int e) {
			start = s;
			end = e;
		}
	}

	public static class EndComparator implements Comparator<Line> {

		@Override
		public int compare (Line o1, Line o2) {
			return o1.end - o2.end;
		}

	}

(2)手动改写堆

  1. 用手写堆实现set(i,v)并排序的功能
  2. 为堆加反向索引实现更多功能并且降低时间复杂度
public class HeapGreater<T> {

	// T 为非基础类型,或者包一层Inner<T>
	private ArrayList<T> heap; // 用Array List数组存放堆结构
	private HashMap<T, Integer> indexMap;  // 存放每个数据的位置,反向索引
	private int heapSize; // 堆大小
	private Comparator<? super T> comp; // 比较器
	
	// 构造方法,只需要传入我们所需要的比较器,剩下的初始化
	public HeapGreater(Comparator<T> c) {
		heap = new ArrayList<>();
		indexMap = new HashMap<>();
		heapSize = 0;
		comp = c;
	}
	//  判断堆是否为空
	public boolean isEmpty() {
		return heapSize == 0;
	}
	// 返回堆的大小
	public int size() {
		return heapSize;
	}
	// 堆中是否包含元素obj
	public boolean contains(T obj) {
		return indexMap.containsKey(obj);
	}
	// 得到堆顶元素
	public T peek() {
		return heap.get(0);
	}
	// 加入元素并进行排序
	public void push(T obj) {
		heap.add(obj);
		indexMap.put(obj, heapSize);
		heapInsert(heapSize++);
	}
	// 弹出堆顶元素并进行重新排序
	public T pop() {
		T ans = heap.get(0);
		swap(0, heapSize - 1);
		indexMap.remove(ans);
		heap.remove(--heapSize);
		heapify(0);
		return ans;
	}
	// 去除指定元素obj,并进行重新排序
	public void remove(T obj) {
		T replace = heap.get(heapSize - 1);
		int index = indexMap.get(obj);
		indexMap.remove(obj);
		heap.remove(--heapSize);
		if (obj != replace) {
			heap.set(index, replace);
			indexMap.put(replace, index);
			resign(replace);
		}
	}
	// 对obj进行堆排序
	public void resign(T obj) {
		heapInsert(indexMap.get(obj));
		heapify(indexMap.get(obj));
	}

	// 请返回堆上的所有元素
	public List<T> getAllElements() {
		List<T> ans = new ArrayList<>();
		for (T c : heap) {
			ans.add(c);
		}
		return ans;
	}
	// 对加入元素进行排序
	private void heapInsert(int index) {
		while (comp.compare(heap.get(index), heap.get((index - 1) / 2)) < 0) {
			swap(index, (index - 1) / 2);
			index = (index - 1) / 2;
		}
	}

	private void heapify(int index) {
		int left = index * 2 + 1;
		while (left < heapSize) {
			int best = left + 1 < heapSize && comp.compare(heap.get(left + 1), heap.get(left)) < 0 ? (left + 1) : left;
			best = comp.compare(heap.get(best), heap.get(index)) < 0 ? best : index;
			if (best == index) {
				break;
			}
			swap(best, index);
			index = best;
			left = index * 2 + 1;
		}
	}

	private void swap(int i, int j) {
		T o1 = heap.get(i);
		T o2 = heap.get(j);
		heap.set(i, o2);
		heap.set(j, o1);
		indexMap.put(o2, i);
		indexMap.put(o1, j);
	}

}

(3)手动改写堆题目练习

给定一个整型数组,int[] arr ;和一个布尔类型数组,boolean[] op,两个数组一定等长,假设长度为N,arr[i]表示客户编号, op[i]表示客户操作
arr = [ 3,3,1,2,1, 2,5…
op = [ T ,T,T, T,F,T,F…
依次表示:3用户购买了一件商品,3用户购买了一件商品,1用户购买了一件商品,2用户购买了一件商品,1用户退货了一件商品,2用户购买了一件商品,5用户退货了一件商品…
一对arr[i]和op[i]就代表一个事件:
用户号为arr[i],op[i] == T 就代表这个用户购买了一件商品
op[i] == F就代表这个用户退货了一件商品
现在你作为电商平台负责人,你想在每一个事件到来的时候,都给购买次数最多的前K名用户颁奖。
所以每个事件发生后,你都需要一个得奖名单(得奖区)。
得奖系统的规则:
1,如果某个用户购买商品数为0,但是又发生了退货事件,则认为该事件无效, 得奖名单和上一个事件发生一致,比如例子中的5用户
2,某用户发生购买商品事件,购买商品数+1,发生退货事件,购买商品数-1
3,每次都是最多K个用户得奖,K也为传入的参数
如果根据全部规则,得奖人数确实不够K个,那就以不够的情况输出结果
4,得奖系统分为得奖区和候选区,任何用户只要购买数>0,一定在这两个区域中的一个
5,购买数最大的前K名用户进入得奖区,在最初时如果得奖区没有到达K个用户,那么新来的用户直接进入得奖区
6,如果购买数不足以进入得奖区的用户,进入候选区
7,如果候选区购买数最多的用户,已经足以进入得奖区,
该用户就会替换得奖区中购买数最少的用户(大于才能替换)
如果得奖区中购买数最少的用户有多个,就替换最早进入得奖区的用户
如果候选区中购买数最多的用户有多个,机会会给最早进入候选区的用户
8, 候选区和得奖区是两套时间,因用户只会在其中一个区域,所以只会有一个区域的时间,另一个没,
从得奖区出来进入候选区的用户,得奖区时间删除,
进入候选区的时间就是当前事件的时间(可 以理解为arr[i]和op[i]中的i)
从候选区出来进入得奖区的用户,候选区时间删除,
进入得奖区的时间就是当前事件的时间(可 以理解为arr[i]和op[i]中的i)
9,如果某用户购买数 == 0,不管在哪个区域都离开,区域时间删除,离开是指彻底离开,哪个区域也不会找到该用户
如果下次该用户又发生购买行为,产生 > 0的购买数,会再次根据之前规则回到某个区域中,进入区域的时间重记

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;

public class Code02_EveryStepShowBoss {

	public static class Customer {
		public int id;
		public int buy;
		public int enterTime;

		public Customer(int v, int b, int o) {
			id = v;
			buy = b;
			enterTime = 0;
		}
	}

	public static class CandidateComparator implements Comparator<Customer> {

		@Override
		public int compare(Customer o1, Customer o2) {
			return o1.buy != o2.buy ? (o2.buy - o1.buy) : (o1.enterTime - o2.enterTime);
		}

	}

	public static class DaddyComparator implements Comparator<Customer> {

		@Override
		public int compare(Customer o1, Customer o2) {
			return o1.buy != o2.buy ? (o1.buy - o2.buy) : (o1.enterTime - o2.enterTime);
		}

	}

	public static class WhosYourDaddy {
		private HashMap<Integer, Customer> customers;
		private HeapGreater<Customer> candHeap;
		private HeapGreater<Customer> daddyHeap;
		private final int daddyLimit;

		public WhosYourDaddy(int limit) {
			customers = new HashMap<Integer, Customer>();
			candHeap = new HeapGreater<>(new CandidateComparator());
			daddyHeap = new HeapGreater<>(new DaddyComparator());
			daddyLimit = limit;
		}

		// 当前处理i号事件,arr[i] -> id,  buyOrRefund
		public void operate(int time, int id, boolean buyOrRefund) {
			if (!buyOrRefund && !customers.containsKey(id)) {
				return;
			}
			if (!customers.containsKey(id)) {
				customers.put(id, new Customer(id, 0, 0));
			}
			Customer c = customers.get(id);
			if (buyOrRefund) {
				c.buy++;
			} else {
				c.buy--;
			}
			if (c.buy == 0) {
				customers.remove(id);
			}
			if (!candHeap.contains(c) && !daddyHeap.contains(c)) {
				if (daddyHeap.size() < daddyLimit) {
					c.enterTime = time;
					daddyHeap.push(c);
				} else {
					c.enterTime = time;
					candHeap.push(c);
				}
			} else if (candHeap.contains(c)) {
				if (c.buy == 0) {
					candHeap.remove(c);
				} else {
					candHeap.resign(c);
				}
			} else {
				if (c.buy == 0) {
					daddyHeap.remove(c);
				} else {
					daddyHeap.resign(c);
				}
			}
			daddyMove(time);
		}

		public List<Integer> getDaddies() {
			List<Customer> customers = daddyHeap.getAllElements();
			List<Integer> ans = new ArrayList<>();
			for (Customer c : customers) {
				ans.add(c.id);
			}
			return ans;
		}

		private void daddyMove(int time) {
			if (candHeap.isEmpty()) {
				return;
			}
			if (daddyHeap.size() < daddyLimit) {
				Customer p = candHeap.pop();
				p.enterTime = time;
				daddyHeap.push(p);
			} else {
				if (candHeap.peek().buy > daddyHeap.peek().buy) {
					Customer oldDaddy = daddyHeap.pop();
					Customer newDaddy = candHeap.pop();
					oldDaddy.enterTime = time;
					newDaddy.enterTime = time;
					daddyHeap.push(newDaddy);
					candHeap.push(oldDaddy);
				}
			}
		}

	}

	public static List<List<Integer>> topK(int[] arr, boolean[] op, int k) {
		List<List<Integer>> ans = new ArrayList<>();
		WhosYourDaddy whoDaddies = new WhosYourDaddy(k);
		for (int i = 0; i < arr.length; i++) {
			whoDaddies.operate(i, arr[i], op[i]);
			ans.add(whoDaddies.getDaddies());
		}
		return ans;
	}

	// 干完所有的事,模拟,不优化
	public static List<List<Integer>> compare(int[] arr, boolean[] op, int k) {
		HashMap<Integer, Customer> map = new HashMap<>();
		ArrayList<Customer> cands = new ArrayList<>();
		ArrayList<Customer> daddy = new ArrayList<>();
		List<List<Integer>> ans = new ArrayList<>();
		for (int i = 0; i < arr.length; i++) {
			int id = arr[i];
			boolean buyOrRefund = op[i];
			if (!buyOrRefund && !map.containsKey(id)) {
				ans.add(getCurAns(daddy));
				continue;
			}
			// 没有发生:用户购买数为0并且又退货了
			// 用户之前购买数是0,此时买货事件
			// 用户之前购买数>0, 此时买货
			// 用户之前购买数>0, 此时退货
			if (!map.containsKey(id)) {
				map.put(id, new Customer(id, 0, 0));
			}
			// 买、卖
			Customer c = map.get(id);
			if (buyOrRefund) {
				c.buy++;
			} else {
				c.buy--;
			}
			if (c.buy == 0) {
				map.remove(id);
			}
			// c
			// 下面做
			if (!cands.contains(c) && !daddy.contains(c)) {
				if (daddy.size() < k) {
					c.enterTime = i;
					daddy.add(c);
				} else {
					c.enterTime = i;
					cands.add(c);
				}
			}
			cleanZeroBuy(cands);
			cleanZeroBuy(daddy);
			cands.sort(new CandidateComparator());
			daddy.sort(new DaddyComparator());
			move(cands, daddy, k, i);
			ans.add(getCurAns(daddy));
		}
		return ans;
	}

	public static void move(ArrayList<Customer> cands, ArrayList<Customer> daddy, int k, int time) {
		if (cands.isEmpty()) {
			return;
		}
		// 候选区不为空
		if (daddy.size() < k) {
			Customer c = cands.get(0);
			c.enterTime = time;
			daddy.add(c);
			cands.remove(0);
		} else { // 等奖区满了,候选区有东西
			if (cands.get(0).buy > daddy.get(0).buy) {
				Customer oldDaddy = daddy.get(0);
				daddy.remove(0);
				Customer newDaddy = cands.get(0);
				cands.remove(0);
				newDaddy.enterTime = time;
				oldDaddy.enterTime = time;
				daddy.add(newDaddy);
				cands.add(oldDaddy);
			}
		}
	}

	public static void cleanZeroBuy(ArrayList<Customer> arr) {
		List<Customer> noZero = new ArrayList<Customer>();
		for (Customer c : arr) {
			if (c.buy != 0) {
				noZero.add(c);
			}
		}
		arr.clear();
		for (Customer c : noZero) {
			arr.add(c);
		}
	}

	public static List<Integer> getCurAns(ArrayList<Customer> daddy) {
		List<Integer> ans = new ArrayList<>();
		for (Customer c : daddy) {
			ans.add(c.id);
		}
		return ans;
	}

9 前缀树、不基于比较的排序、排序稳定性、排序总结

(1)前缀树(prefix tree trie)

实现两种功能:

  • 查询以“XXX”字符串为前缀的数量pass
  • 查询“XXX”字符串出现的次数记为end

哈希表的增删改查时间复杂度不一定是O(1),看单样本长度
前缀树形成的时间复杂度O(M),M为字符串总长度,查询字符串出现次数时间复杂度为O(K),K为字符串长度,同理查询前缀的时间复杂度也跟被查询字符串的长度有关。

public static class Node1 {
		public int pass;
		public int end;
		public Node1[] nexts;

		// char tmp = 'b'  (tmp - 'a')
		public Node1() {
			pass = 0;
			end = 0;
			// 0    a,1    b,2    c,..   ..,,25   z
			// nexts[i] == null   i方向的路不存在, nexts[i] != null   i方向的路存在
			nexts = new Node1[26]; // 26个字母长度,存放node节点的数组
		}
	}

	public static class Trie1 {
		private Node1 root;
		public Trie1() {
			root = new Node1();
		}
		// 生成前缀树
		public void insert (String word) {
			if (word == null) {
				return;
			}
			char[] str = word.toCharArray (); // toCharArray()方法返回字符串数组
			Node1 node = root;
			node.pass++;
			int path = 0;
			for (int i = 0; i < str.length; i++) { // 从左往右遍历字符
				path = str[i] - 'a'; // 由字符,对应成走向哪条路
				if (node.nexts[path] == null) {
					node.nexts[path] = new Node1();
				}
				node = node.nexts[path]; // node 向下沉
				node.pass++;
			}
			node.end++;
		}
		// 删除某个字符串,防止内存泄漏
		public void delete (String word) {
			if (search (word) != 0) {
				char[] chs = word.toCharArray ();
				Node1 node = root;
				node.pass--;
				int path = 0;
				for (int i = 0; i < chs.length; i++) {
					path = chs[i] - 'a';
					if (--node.nexts[path].pass == 0) {
						node.nexts[path] = null;
						return;
					}
					node = node.nexts[path];
				}
				node.end--;
			}
		}

		// word这个单词之前加入过几次
		public int search (String word) {
			if (word == null) {
				return 0;
			}
			char[] chs = word.toCharArray ();
			Node1 node = root;
			int index = 0;
			for (int i = 0; i < chs.length; i++) {
				index = chs[i] - 'a';
				if (node.nexts[index] == null) {
					return 0;
				}
				node = node.nexts[index];
			}
			return node.end;
		}

		// 所有加入的字符串中,有几个是以pre这个字符串作为前缀的
		public int prefixNumber (String pre) {
			if (pre == null) {
				return 0;
			}
			char[] chs = pre.toCharArray ();
			Node1 node = root;
			int index = 0;
			for (int i = 0; i < chs.length; i++) {
				index = chs[i] - 'a';
				if (node.nexts[index] == null) {
					return 0;
				}
				node = node.nexts[index];
			}
			return node.pass;
		}
	}

(2)不基于比较的排序(桶排序)

计数排序

  • 不基于比较的排序时间复杂度最小O(N),基于比较的排序O(N*logN)
  • 数组中所有的树为员工的年龄,由小到大排序,出现样本数据的状况
    思路
    准备一个help数组,数组下标为[0-200]将0-200岁的数据依次遍历 ,出现的次数放入与对象年纪相等的数组下标中,然后遍历help数组一遍即为所求。O(N),每一个年龄是一个桶。
// only for 0~200 value
	public static void countSort (int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		int max = Integer.MIN_VALUE;
		for (int i = 0; i < arr.length; i++) {
			max = Math.max (max, arr[i]);
		}
		int[] bucket = new int[max + 1];
		for (int i = 0; i < arr.length; i++) {
			bucket[arr[i]]++;
		}
		int i = 0;
		for (int j = 0; j < bucket.length; j++) {
			while (bucket[j]-- > 0) {
				arr[i++] = j;
			}
		}
	}

基数排序

条件:非负的,能用十进制表示的(如果是负数,找到最小的数字,然后加上这个数的正数,最后在减去,但是有边界问题)
思路
准备0-9个桶,先取所有数据的个位数,依次放入桶中,然后按顺序从小到大弹出,在将数据中十位数依次放入桶中,再按从小到大弹出,最后按照百位数放入桶中,同理,最后弹出的顺序即为所求(总结:依次放进桶,依次排序,但以高位为主)

// only for no-negative value
	public static void radixSort (int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		radixSort (arr, 0, arr.length - 1, maxbits (arr));
	}
	// 计算最大数是几位
	public static int maxbits (int[] arr) {
		int max = Integer.MIN_VALUE;
		for (int i = 0; i < arr.length; i++) {
			max = Math.max (max, arr[i]);
		}
		int res = 0;
		while (max != 0) {
			res++;
			max /= 10;
		}
		return res;
	}
	// 对任意数组下标进行排序
	// arr[L..R]排序  ,  最大值的十进制位数digit
	public static void radixSort (int[] arr, int L, int R, int digit) {
		final int radix = 10;
		int i = 0, j = 0;
		// 有多少个数准备多少个辅助空间
		int[] help = new int[R - L + 1];
		for (int d = 1; d <= digit; d++) { // 有多少位就进出几次
			// 10个空间
		    // count[0] 当前位(d位)是0的数字有多少个
			// count[1] 当前位(d位)是(0和1)的数字有多少个
			// count[2] 当前位(d位)是(0、1和2)的数字有多少个
			// count[i] 当前位(d位)是(0~i)的数字有多少个
			int[] count = new int[radix]; // count[0..9]
			// count数组,例如[1,3,2,0,0,0],0结尾的有一个,1结尾有三个,2结尾有两个
			for (i = L; i <= R; i++) {
				// 103  1   3
				// 209  1   9
				j = getDigit (arr[i], d);
				count[j]++;
			}
			// count'数组,例如[1,4,6,6,6,6],依次相加
			for (i = 1; i < radix; i++) {
				count[i] = count[i] + count[i - 1];
			}
			// 从右到左依次放入
			for (i = R; i >= L; i--) {
				j = getDigit (arr[i], d);
				help[count[j] - 1] = arr[i];
				count[j]--;
			}
			// 从右到左依次放入
			for (i = L, j = 0; i <= R; i++, j++) {
				arr[i] = help[j];
			}
		}
	}

	public static int getDigit (int x, int d) {
		return ((x / ((int) Math.pow (10, d - 1))) % 10);
	}

(4)排序算法的稳定性

计数排序和基数排序
1)一般来讲,计数排序要求,样本是整数,且范围比较窄
2)一般来讲, 基数排序要求,样本是10进制的正整数
一旦要求稍有升级,改写代价增加是显而易见的

排序算法的稳定性
稳定性是指同样大小的样本再排序之后不会改变相对次序
对基础类型来说,稳定性毫无意义
对非基础类型来说,稳定性有重要意义
有些排序算法可以实现成稳定的,而有些排序算法无论如何都实现不成稳定的

排序算法总结

时间复杂度额外空间复杂度稳定性
选择排序O(N^2)O(1)
冒泡排序O(N^2)O(1)
插入排序O(N^2)O(1)
归并排序O(N*logN)O(N)
随机排序O(N^logN)O(logN)
堆排序O(N^logN)O(1)
计数排序O(N)O(M)
基数排序O(N)O(N)

排序算法总结
1)不基于比较的排序,对样本数据有严格要求,不易改写
2) 基于比较的排序,只要规定好两个样本怎么比大小就可以直接复用
3)基于比较的排序,时间复杂度的极限是O(NlogN)
4)时间复杂度O(N
logN)、 额外空间复杂度低于O(N)、 且稳定的基于比较的排序是不存在的。
5) 为了绝对的速度选快排、为了省空间选堆排、为了稳定性选归并

10 链表相关coding练习题(边界条件)

快慢指针练习(单链表)
用快慢指针和数个数两种方法实现下面问题:
1)输入链表头节点,奇数长度返回中点,偶数长度返回上中点
2)输入链表头节点,奇数长度返回中点,偶数长度返回下中点
3)输入链表头节点,奇数长度返回中点前一个,偶数长度返回上中点前一个
4)输入链表头节点,奇数长度返回中点前一个,偶数长度返回下中点前一个

public static class Node {
		public int value;
		public Node next;

		public Node(int v) {
			value = v;
		}
	}

	// head 头
	public static Node midOrUpMidNode (Node head) {
		if (head == null || head.next == null || head.next.next == null) {
			return head;
		}
		// 链表有3个点或以上
		Node slow = head.next;
		Node fast = head.next.next;
		while (fast.next != null && fast.next.next != null) {
			slow = slow.next;
			fast = fast.next.next;
		}
		return slow;
	}

	public static Node midOrDownMidNode (Node head) {
		if (head == null || head.next == null) {
			return head;
		}
		Node slow = head.next;
		Node fast = head.next;
		while (fast.next != null && fast.next.next != null) {
			slow = slow.next;
			fast = fast.next.next;
		}
		return slow;
	}

	public static Node midOrUpMidPreNode (Node head) {
		if (head == null || head.next == null || head.next.next == null) {
			return null;
		}
		Node slow = head;
		Node fast = head.next.next;
		while (fast.next != null && fast.next.next != null) {
			slow = slow.next;
			fast = fast.next.next;
		}
		return slow;
	}

	public static Node midOrDownMidPreNode (Node head) {
		if (head == null || head.next == null) {
			return null;
		}
		if (head.next.next == null) {
			return head;
		}
		Node slow = head;
		Node fast = head.next;
		while (fast.next != null && fast.next.next != null) {
			slow = slow.next;
			fast = fast.next.next;
		}
		return slow;
	}

	public static Node right1 (Node head) {
		if (head == null) {
			return null;
		}
		Node cur = head;
		ArrayList<Node> arr = new ArrayList<>();
		while (cur != null) {
			arr.add (cur);
			cur = cur.next;
		}
		return arr.get ((arr.size () - 1) / 2);
	}

	public static Node right2 (Node head) {
		if (head == null) {
			return null;
		}
		Node cur = head;
		ArrayList<Node> arr = new ArrayList<>();
		while (cur != null) {
			arr.add (cur);
			cur = cur.next;
		}
		return arr.get (arr.size () / 2);
	}

	public static Node right3 (Node head) {
		if (head == null || head.next == null || head.next.next == null) {
			return null;
		}
		Node cur = head;
		ArrayList<Node> arr = new ArrayList<>();
		while (cur != null) {
			arr.add (cur);
			cur = cur.next;
		}
		return arr.get ((arr.size () - 3) / 2);
	}

	public static Node right4 (Node head) {
		if (head == null || head.next == null) {
			return null;
		}
		Node cur = head;
		ArrayList<Node> arr = new ArrayList<>();
		while (cur != null) {
			arr.add (cur);
			cur = cur.next;
		}
		return arr.get ((arr.size () - 2) / 2);
	}

面试时链表解题的方法论
1)对于笔试,不用太在乎空间复杂度,一切为了时间复杂度
2)对于面试,时间复杂度依然放在第一位,但是一定要找到空间最省的方法
链表面试题常用数据结构和技巧
1)使用容器(哈希表、数组等)
2)快慢指针
常见面试题
1.给定一个单链表的头节点head,请判断该链表是否为回文结构(正念和反念一样:12321)。
1)哈希表方法特别简单(笔试用)
2)改原链表的方法就需要注意边界了(面试用)
思路:
1)用栈结构,遍历一个弹出一个,比较是否一样。
2)奇数个时,取中间数,将它下一个指针指向null,再将后面的数指向取反,然后左右头节点同时向中间遍历比较,直到遍历为空,偶数个/2-1个设为null,同理,直到有一个为null停止,返回yes or no前把链表复原。

public static class Node {
		public int value;
		public Node next;

		public Node(int data) {
			this.value = data;
		}
	}

	// need n extra space,利用栈容器
	public static boolean isPalindrome1 (Node head) {
		Stack<Node> stack = new Stack<Node>();
		Node cur = head;
		while (cur != null) {
			stack.push (cur);
			cur = cur.next;
		}
		while (head != null) {
			if (head.value != stack.pop ().value) {
				return false;
			}
			head = head.next;
		}
		return true;
	}
	
	// need O(1) extra space
	public static boolean isPalindrome3 (Node head) {
		if (head == null || head.next == null) {
			return true;
		}
		Node n1 = head;
		Node n2 = head;
		while (n2.next != null && n2.next.next != null) { // find mid node
			n1 = n1.next; // n1 -> mid
			n2 = n2.next.next; // n2 -> end
		}
		// n1 中点
		
		
		n2 = n1.next; // n2 -> right part first node
		n1.next = null; // mid.next -> null
		Node n3 = null;
		while (n2 != null) { // right part convert
			n3 = n2.next; // n3 -> save next node
			n2.next = n1; // next of right node convert
			n1 = n2; // n1 move
			n2 = n3; // n2 move
		}
		n3 = n1; // n3 -> save last node
		n2 = head;// n2 -> left first node
		boolean res = true;
		while (n1 != null && n2 != null) { // check palindrome
			if (n1.value != n2.value) {
				res = false;
				break;
			}
			n1 = n1.next; // left to mid
			n2 = n2.next; // right to mid
		}
		n1 = n3.next;
		n3.next = null;
		while (n1 != null) { // recover list
			n2 = n1.next;
			n1.next = n3;
			n3 = n1;
			n1 = n2;
		}
		return res;
	}

2.将单向链表按某值划分成左边小、中间相等、右边大的形式
1)把链表放入数组里,在数组上做partition (笔试用)
2) 分成小、中、大三部分,再把各个部分之间串起来(面试用)
思路:
用六个引用,小头、小尾、等头、等尾、大头、大尾分别存放小区的头节点、小区的尾节点… …(存在尾节点是为了保证排序的稳定性)最后将小尾与等头,等尾与大头相连。

	public static Node listPartition2 (Node head, int pivot) {
		Node sH = null; // small head
		Node sT = null; // small tail
		Node eH = null; // equal head
		Node eT = null; // equal tail
		Node mH = null; // big head
		Node mT = null; // big tail
		Node next = null; // save next node
		// every node distributed to three lists
		while (head != null) {
			next = head.next;
			head.next = null;
			if (head.value < pivot) {
				if (sH == null) {
					sH = head;
					sT = head;
				} else {
					sT.next = head;
					sT = head;
				}
			} else if (head.value == pivot) {
				if (eH == null) {
					eH = head;
					eT = head;
				} else {
					eT.next = head;
					eT = head;
				}
			} else {
				if (mH == null) {
					mH = head;
					mT = head;
				} else {
					mT.next = head;
					mT = head;
				}
			}
			head = next;
		}
		// 小于区域的尾巴,连等于区域的头,等于区域的尾巴连大于区域的头
		if (sT != null) { // 如果有小于区域
			sT.next = eH;
			eT = eT == null ? sT : eT; // 下一步,谁去连大于区域的头,谁就变成eT
		}
		// 下一步,一定是需要用eT 去接 大于区域的头
		// 有等于区域,eT -> 等于区域的尾结点
		// 无等于区域,eT -> 小于区域的尾结点
		// eT 尽量不为空的尾巴节点,既没有小于也没有等于
		if (eT != null) { // 如果小于区域和等于区域,不是都没有
			eT.next = mH;
		}
		return sH != null ? sH : (eH != null ? eH : mH);
	}

	public static void printLinkedList (Node node) {
		System.out.print ("Linked List: ");
		while (node != null) {
			System.out.print (node.value + " ");
			node = node.next;
		}
		System.out.println ();
	}

3.一种特殊的单链表节点类描述如下
class Node {
int value;
Node next;
Node rand;
Node(int val) { value = val;}
rand指针是单链表节点结构中新增的指针,rand 可能指向链表中的任意-个节点,也可能指向null。
给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。
[要求]
时间复杂度0(N),额外空间复杂度0(1)
思路
用may结构,key存放老节点,value存放复制老节点的新节点,目的是每一个老节点都能找到对应的新节点。然后建立新节点的next和random节点的时候,去查询key中老节点的next与random

public static class Node {
		int val;
		Node next;
		Node random;

		public Node(int val) {
			this.val = val;
			this.next = null;
			this.random = null;
		}
	}

	public static Node copyRandomList1 (Node head) {
		// key 老节点
		// value 新节点
		HashMap<Node, Node> map = new HashMap<Node, Node>();
		Node cur = head;
		while (cur != null) {
			map.put (cur, new Node(cur.val));
			cur = cur.next;
		}
		cur = head;
		while (cur != null) {
			// cur 老
			// map.get(cur) 新
			// 新.next ->  cur.next克隆节点找到
			map.get (cur).next = map.get (cur.next);
			map.get (cur).random = map.get (cur.random);
			cur = cur.next;
		}
		return map.get (head);
	}

	public static Node copyRandomList2 (Node head) {
		if (head == null) {
			return null;
		}
		Node cur = head;
		Node next = null;
		// 1 -> 2 -> 3 -> null
		// 1 -> 1' -> 2 -> 2' -> 3 -> 3'
		while (cur != null) {
			next = cur.next;
			cur.next = new Node(cur.val);
			cur.next.next = next;
			cur = next;
		}
		cur = head;
		Node copy = null;
		// 1 1' 2 2' 3 3'
		// 依次设置 1' 2' 3' random指针
		while (cur != null) {
			next = cur.next.next;
			copy = cur.next;
			copy.random = cur.random != null ? cur.random.next : null;
			cur = next;
		}
		Node res = head.next;
		cur = head;
		// 老 新 混在一起,next方向上,random正确
		// next方向上,把新老链表分离
		while (cur != null) {
			next = cur.next.next;
			copy = cur.next;
			cur.next = next;
			copy.next = next != null ? next.next : null;
			cur = next;
		}
		return res;
	}

4.给定两个可能有环也可能无环的单链表,头节点head1和head2。
请实现一个函数,如果两个链表相交,请返回相交的第个节点。
如果不相交,返回null
[要求]
如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复
杂度请达到O(1)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值