Вопрос интервью: удалить дубликаты из несортированного связанного списка
Я читаю взлом кодирования интервью, четвертое издание: 150 Программирование интервью вопросы и решения и я пытаюсь решить следующий вопрос:
2.1 напишите код для удаления дубликатов из несортированного связанного списка. СЛЕДОВАТЬ UP: как бы вы решили эту проблему, если временный буфер не разрешен?
Я решаю его на C#, поэтому я сделал свой собственный Node
класс:
public class Node<T> where T : class
{
public Node<T> Next { get; set; }
public T Value { get; set; }
public Node(T value)
{
Next = null;
Value = value;
}
}
мое решение повторите список, затем для каждого узла повторите оставшуюся часть списка и удалите все дубликаты (обратите внимание, что я фактически не скомпилировал или не протестировал это, как указано в книге):
public void RemoveDuplicates(Node<T> head)
{
// Iterate through the list
Node<T> iter = head;
while(iter != null)
{
// Iterate to the remaining nodes in the list
Node<T> current = iter;
while(current!= null && current.Next != null)
{
if(iter.Value == current.Next.Value)
{
current.Next = current.Next.Next;
}
current = current.Next;
}
iter = iter.Next;
}
}
вот решение из книги (автор написал его на java):
без буфера, мы можем пройтись с два указателя: "current" делает нормальный итерация, в то время как "runner" повторяет через все предыдущие узлы для проверки пакет DUP. Бегун только один dup в узел, потому что если было несколько дубликаты они бы уже снято.
public static void deleteDups2(LinkedListNode head)
{
if (head == null) return;
LinkedListNode previous = head;
LinkedListNode current = previous.next;
while (current != null)
{
LinkedListNode runner = head;
while (runner != current) { // Check for earlier dups
if (runner.data == current.data)
{
LinkedListNode tmp = current.next; // remove current
previous.next = tmp;
current = tmp; // update current to next node
break; // all other dups have already been removed
}
runner = runner.next;
}
if (runner == current) { // current not updated - update now
previous = current;
current = current.next;
}
}
}
так что мое решение всегда ищет дубликаты для текущего узла до конца, в то время как их решение, ищет дубликаты от головы до текущего узла. Я чувствую, что оба решения будут испытывать проблемы с производительностью в зависимости от того, сколько дубликатов есть в списке и как они распределены (плотность и положение). Но в целом: это мой ответ почти так же хорошо, как в книге, или значительно хуже?
11 ответов
Если вы дадите человеку рыбу, они едят в течение дня. Если научить человека рыбачить...
мои меры по качеству реализации:
- достоверность: если вы не получаете правильный ответ во всех случаях, то он не готов
- читаемость/ремонтопригодность: посмотрите на повторение кода, понятные имена, количество строк кода на блок / метод (и количество вещей, которые делает каждый блок), и как трудно отследить поток вашего кода. Посмотрите на любое количество книг, посвященных рефакторингу, лучшим методам программирования, стандартам кодирования и т. д., Если вы хотите получить больше информации об этом.
- теоретическая производительность (в худшем случае и ammortized): Big-O - это метрика, которую вы можете использовать. CPU и потребление памяти должны быть измерены
- сложности: оцените, как это займет средний профессиональный программист для реализации (если они уже знают алгоритм). Посмотрите, соответствует ли это тому, насколько трудна проблема на самом деле
Что касается вашей реализации:
- достоверность: я предлагаю написать модульные тесты, чтобы определить это для себя и/или отладить его (на бумаге) от начала до конца с интересными примерами / крайними случаями. Null, один элемент, два элемента, различные числа дубликатов и т. д.
- читаемость/ремонтопригодность: это выглядит в основном нормально, хотя ваши последние два комментария ничего не добавляют. Немного более очевидно, что делает ваш код, чем код в книге
- производительность: я считаю, что оба N-квадрат. Является ли амортизированная стоимость ниже на одном или другом, я позволю вам выяснить:)
- срок реализации: средний профессионал должен уметь кодировать этот алгоритм во сне, поэтому хорошо выглядит
нет большой разницы. Если я правильно сделал математику, ваши в среднем N / 16 медленнее, чем авторы, но существует множество случаев, когда ваша реализация будет быстрее.
Edit:
Я назову вашу реализацию Y, а автора a
оба предложенных решения имеют o(n^2) в худшем случае, и оба они имеют лучший случай O (N), когда все элементы имеют одинаковое значение.
EDIT: Это полный переписывать. Вдохновленный дебатом в комментариях, я попытался найти средний случай для случайных N случайных чисел. Это последовательность со случайным размером и случайным распределением. Каков будет средний случай.
Y всегда будет работать U раз, где U-количество уникальных чисел. Для каждой итерации он будет делать N-x сравнения, где X-количество элементов, удаленных до итерации (+1). В первый раз ни один элемент не будет удален и в среднем на второй итерации N / U будет удален.
то есть в среднем ½N останется для итерации. Мы можем выразить среднюю стоимость, U * ½N. Среднее значение U может быть выражено на основе N, а также 0
выражение A становится более сложным. Предположим, мы используем итерации I, Прежде чем встретим все уникальные значения. После этого будет выполняться между сравнениями 1 и U (в среднем это U/") и будет делать это N-I раз.
I * c+U / 2 (N-I)
но какое среднее число сравнения (c) мы запускаем для первых I итераций. в среднем нам нужно сравнить с половиной элементов, которые мы уже посетили, и в среднем мы посетили I / 2 элемента, т. е. c=I / 4
I/4+U / 2(N-I).
Я могу быть выражен в терминах N. В среднем нам нужно будет посетить половину на N, чтобы найти уникальные значения, поэтому I=N / 2 дает среднее значение
(I^2)/4+U/2 (N-I), который можно свести к (3*N^2)/16.
Это, конечно, если моя оценка средние значения верны. То есть в среднем для любой потенциальной последовательности A имеет N/16 меньше сравнений, чем Y, но существует множество случаев, когда Y быстрее A. Поэтому я бы сказал, что они равны по сравнению с количеством сравнений
Как насчет использования HashMap? Таким образом, это займет O(n) время и o(n) пространство. Я напишу psuedocode.
function removeDup(LinkedList list){
HashMap map = new HashMap();
for(i=0; i<list.length;i++)
if list.get(i) not in map
map.add(list.get(i))
else
list.remove(i)
end
end
end
конечно, мы предполагаем, что HashMap имеет O(1) Чтение и запись.
другим решением является использование mergesort и удаление дубликатов из начала и конца списка. Это занимает O (N log n)
mergesort - O (N log n) удаление дубликата из отсортированного списка равно O (n). знаешь почему? поэтому вся операция принимает O (N log n)
heapsort как - это на месте сортировка. Вы можете изменить функцию "siftUp" или "siftDown", чтобы просто удалить элемент, если он сталкивается с равным родителем. Это будет O (N log n)
function siftUp(a, start, end) is
input: start represents the limit of how far up the heap to sift.
end is the node to sift up.
child := end
while child > start
parent := floor((child - 1) ÷ 2)
if a[parent] < a[child] then (out of max-heap order)
swap(a[parent], a[child])
child := parent (repeat to continue sifting up the parent now)
else if a[parent] == a[child] then
remove a[parent]
else
return
ваше решение так же хорошо, как и авторское, только у него есть ошибка в реализации :) попробуйте проследить его по списку из двух узлов с равными данными.
ваш подход просто зеркальный к книге! Вы идете вперед, книга идет назад. Нет никакой разницы, поскольку вы оба сканируете все элементы. И да, поскольку буфер не разрешен, возникают проблемы с производительностью. Вы обычно не должны помнить о работе с такими costrained вопросы и, когда это явно не требуется.
интервью вопросы сделаны, чтобы проверить вашу открытость. У меня есть сомнения в ответе Марка: это определенно лучшее решение в примеры реального мира, но даже если эти алгоритмы используют постоянное пространство, ограничение, которое нет временный буфер разрешен должен соблюдаться.
в противном случае, я думаю, что книга приняла бы такой подход. Марк, пожалуйста, прости меня за критику.
в любом случае, чтобы углубиться в этот вопрос, ваш и подход книги требуют Theta(n^2)
время, в то время как подход Марка требует Theta(n logn) + Theta(n)
время, которое приводит к Theta(n logn)
. Почему? Theta
? Потому что алгоритмы сравнения-свопа являются Omega(n logn)
тоже помню!
код на java:
public static void dedup(Node head) {
Node cur = null;
HashSet encountered = new HashSet();
while (head != null) {
encountered.add(head.data);
cur = head;
while (cur.next != null) {
if (encountered.contains(cur.next.data)) {
cur.next = cur.next.next;
} else {
break;
}
}
head = cur.next;
}
}
пробовал то же самое в cpp. Пожалуйста, дайте мне знать ваши комментарии по этому поводу.
// ConsoleApplication2.cpp : определяет точку входа для консольного приложения. //
#include "stdafx.h"
#include <stdlib.h>
struct node
{
int data;
struct node *next;
};
struct node *head = (node*)malloc(sizeof(node));
struct node *tail = (node*)malloc(sizeof(node));
struct node* createNode(int data)
{
struct node *newNode = (node*)malloc(sizeof(node));
newNode->data = data;
newNode->next = NULL;
head = newNode;
return newNode;
}
bool insertAfter(node * list, int data)
{
//case 1 - insert after head
struct node *newNode = (node*)malloc(sizeof(node));
if (!list)
{
newNode->data = data;
newNode->next = head;
head = newNode;
return true;
}
struct node * curpos = (node *)malloc(sizeof(node));
curpos = head;
//case 2- middle, tail of list
while (curpos)
{
if (curpos == list)
{
newNode->data = data;
if (curpos->next == NULL)
{
newNode->next = NULL;
tail = newNode;
}
else
{
newNode->next = curpos->next;
}
curpos->next = newNode;
return true;
}
curpos = curpos->next;
}
}
void deleteNode(node *runner, node * curr){
//DELETE AT TAIL
if (runner->next->next == NULL)
{
runner->next = NULL;
}
else//delete at middle
{
runner = runner->next->next;
curr->next = runner;
}
}
void removedups(node * list)
{
struct node * curr = (node*)malloc(sizeof(node));
struct node * runner = (node*)malloc(sizeof(node));
curr = head;
runner = curr;
while (curr != NULL){
runner = curr;
while (runner->next != NULL){
if (curr->data == runner->next->data){
deleteNode(runner, curr);
}
if (runner->next!=NULL)
runner = runner->next;
}
curr = curr->next;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
struct node * list = (node*) malloc(sizeof(node));
list = createNode(1);
insertAfter(list,2);
insertAfter(list, 2);
insertAfter(list, 3);
removedups(list);
return 0;
}
код в C:
void removeduplicates(N **r)
{
N *temp1=*r;
N *temp2=NULL;
N *temp3=NULL;
while(temp1->next!=NULL)
{
temp2=temp1;
while(temp2!=NULL)
{
temp3=temp2;
temp2=temp2->next;
if(temp2==NULL)
{
break;
}
if((temp2->data)==(temp1->data))
{
temp3->next=temp2->next;
free(temp2);
temp2=temp3;
printf("\na dup deleted");
}
}
temp1=temp1->next;
}
}
вот ответ в C
void removeduplicates(N **r)
{
N *temp1=*r;
N *temp2=NULL;
N *temp3=NULL;
while(temp1->next!=NULL)
{
temp2=temp1;
while(temp2!=NULL)
{
temp3=temp2;
temp2=temp2->next;
if(temp2==NULL)
{
break;
}
if((temp2->data)==(temp1->data))
{
temp3->next=temp2->next;
free(temp2);
temp2=temp3;
printf("\na dup deleted");
}
}
temp1=temp1->next;
}
}
C# код для удаления дубликатов осталось после первой итерации:
public Node removeDuplicates(Node head)
{
if (head == null)
return head;
var current = head;
while (current != null)
{
if (current.next != null && current.data == current.next.data)
{
current.next = current.next.next;
}
else { current = current.next; }
}
return head;
}